Commit Graph
540 Commits
Author SHA1 Message Date
dotCooCoo d9d61ee8da Fields: several capitalization switches apply in the order they appear
A field with \* Lower \* Caps applied only \* Lower, so a title of
WEEKLY REPORT showed as weekly report. Every capitalization switch in a
field code now applies in turn, and the example shows Weekly Report.
2026-10-24 19:39:35 -07:00
dotCooCoo 21d96baf98 Merge branch 'feat/document-information-fields' into fix/field-text-format-switches 2026-10-24 19:37:41 -07:00
dotCooCoo 86ccf6cf3d Fields: only quoted text after a summary field's name replaces the property
SUBJECT Draft left the field showing Draft and set the document's
subject to it. A TITLE, SUBJECT, AUTHOR, KEYWORDS or COMMENTS field now
replaces its property only when quoted text follows the field's name,
and otherwise shows the property as it is.
2026-10-24 17:55:11 -07:00
dotCooCoo c41251238b Fields: \* Upper, \* Lower, \* Caps and \* FirstCap change the result, and CardText is lowercase
Update Field and the page-number fields ignored the capitalization
switches, so TITLE \* Upper or FILENAME \* Lower showed the text as
stored. A \* CardText or \* OrdText result started with a capital
("Twelve"), where Word shows it in lowercase unless a capitalization
switch follows. A table formula with a \* switch, such as
=SUM(A1:B2) \* CardText or =SUM(ABOVE) \* MERGEFORMAT, was a syntax
error.

The results of the document information fields, DOCPROPERTY,
FILENAME, the dates, REF, PAGEREF, SEQ, STYLEREF and table formulas
now take the field's \* Upper, \* Lower, \* Caps or \* FirstCap switch
when Update Field computes them. PAGE, NUMPAGES, SECTION,
SECTIONPAGES, PAGEREF and STYLEREF take it when they are laid out.
Cardinal and ordinal text is lowercase ("twelve", "seventh") unless
one of the switches follows. WordCraft now reads a table formula's \*
switches separately from the formula. A whole-number result that a \*
number format such as CardText or roman covers is written in that
format; any other result, and any other \* switch, leaves the digits.
Table Layout › Data › Formula keeps the \* switches as typed.
2026-10-24 17:02:51 -07:00
dotCooCoo eccb2fc230 Fields: SUBJECT, KEYWORDS, COMMENTS, LASTSAVEDBY, REVNUM and NUMCHARS update with the others
Update Field left the SUBJECT, KEYWORDS, COMMENTS, LASTSAVEDBY, REVNUM
and NUMCHARS fields at their stored result, so one inserted in
WordCraft showed nothing, and the Field dialog did not list them.
These fields now update with the others and are listed under Document
Information.

A quoted text after TITLE, SUBJECT, AUTHOR, KEYWORDS or COMMENTS
replaces that property, at most 255 characters, as in Word, and later
fields, DOCPROPERTY included, show the new value. NUMWORDS, NUMCHARS
and REVNUM take a \* number format.
2026-10-24 15:22:57 -07:00
Federico Olivares af33e27e13 Merge origin/main 2026-10-24 14:34:57 -03:00
+39 254351c01b Insert › Table: the grid previews the hovered table on the page (#3931) (#4052)
* Insert › Table: the grid previews the hovered table on the page (#3931)

* Insert › Table preview: the test checks the table lands at the caret; a new hovered size repaints the page (#3931)

* Insert › Table preview: the pages are drawn from the copy the preview laid out, whose paragraph paths are the layout's (#3931)

* Insert › Table preview: a caret move or a view change with the grid still hovered lays the preview out again (#3931)

* Insert › Table preview: the caret and selection are drawn from the previewed copy, Reading Highlight waits for the preview to end, and the word being typed stays unmarked on it (#3931)

* Format negative zero as 0 in spreadsheet import (#4040)

Found while testing crates/formats/src/sheet.rs:726-727 (fn number)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Ignore quoted text when detecting recipient CSV delimiter (#4125)

Found while testing crates/engine/src/cmd/mailings.rs:526-527 (parse_csv)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap label and envelope text length in mailings commands (#4008)

Fixes #3633

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* DOCX: a hinted East Asian or complex-script font no longer replaces the Latin font (fixes #4031) (#4067)

* Phonetic Guide dialog puts the keyboard in the Ruby text box on open (#3914) (#4021)

* Route ui.screenshot path writes through the guarded engine writer (#4048)

Fixes #3635

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Dialogs: draw real tabs (Paragraph, Find, About) (#4029)

* Dialog tabs: draw real tabs in Paragraph, Find and About (#4026)

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

* A word with no break opportunity in it makes the layout quadratic (#3808)

* A word with no break opportunity in it no longer makes the layout quadratic

Laying out a line beside a floating object measures the word that starts on
it, to tell whether the word fits the full line. The measurement walked to
the end of the paragraph every time, so a word with no break opportunity in
it — which takes a line per few characters — walked the paragraph once per
line, and the layout was quadratic in the word's length.

The measurement now stops once the word is clearly wider than the room there
is: the answer is known by then, and the advances are widths, so the rest of
the word could only make it wider — unless a font pulls a glyph back with a
negative advance, and then the measurement carries on only as far as the rest
of the word could take the width back.

The same answer, the same layout, one line's worth of clusters per line
instead of a paragraph's. A 1 MiB word went from 29.42 s to 0.35 s, and a
4 MiB one from 472.34 s to 1.38 s.

#3412

* Record the layout fix in the progress docs

The layout parity table gains the measurement for a word with no break
opportunity in it laid out beside a floating object, and a note says what
the fix was. The ranked gap list holds one row per gap now, and its #20 row
carries the measured figure. The progress log and the revision history gain
the entry.

#3412

* Measure every word once, before the line is broken (#3412)

The measurement of the word that starts a line walked the word to find out how
wide it was, and a font that pulls a glyph back with a negative advance keeps
the walk going: the bound that stopped it early was the sum of the advances the
rest of the word could give back, and when the advances alternate in sign that
sum stays so negative that the walk never stops early.

Every word is measured once now, before the line is broken: the width of a word
is the sum of its advances, the negative ones included, and asking whether the
word fits the full line is a lookup rather than another pass over it.

The answer is the same: the sum is taken over the same clusters the scan took
them over. It is added up from the word's end rather than its start, which is
the same number in all but the last bits of a float — the sum a float holds
depends on the order it is added in, and the two differ by far less than the
tenth of a point the comparison that uses the width allows.

The ranked-gap rows that were not this fix's are back to how main has them.

* Lay the negative-advance word out beside a float (#3412)

The width of a word is checked against the width of a word, and a font that
pulls a glyph back takes the width off the rest of the word as well. The
layout now does that too: the word is laid out beside a floating object, in
the strip the object leaves, and it fits there — where the same word without
the glyph pulled back does not, and moves on whole below the object instead.

The strip the object leaves is half way between the two widths, so the test
does not depend on the font's metrics.

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document ei…

* Outline: re-match collapsed headings only after edits, via a hash index (#4014)

* Outline: re-match collapsed headings only after edits, via a hash index

Fixes #3621

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

* Write a text-box story shared by many shapes only once on docx save (#3983)

Fixes #3653

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Keep pending formatting revisions when Style Inspector clears formatting (#3971)

Fixes #3671

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the headless ui_requests queue and keep one Paste Special request (#3997)

Fixes #3641

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Validate PNG extension in file.exportPng (#3837)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* changes to filter dialogue

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

C…

* DOCX: write the default font size when the document leaves it unset (#4092)

A Document whose styles.default_chr.size is None resolves its text at 11 pt
(resolve.rs falls back to 11.0), but the writer left w:sz out of
w:docDefaults/w:rPrDefault and the reader fills an omitted size with 10 pt.
So WordCraft's own save and reopen changed the effective default size from
11 pt to 10 pt with no edit in between.

Name the resolver's fallback DEFAULT_FONT_SIZE, use it in both resolver sites,
and have the writer emit it when the default size is unset. The reader is
unchanged, so a file from another program that has no size is read exactly as
before. The written size is the resolver's final fallback and not what Normal
resolves to, so a style that sets a size is unaffected.

Adds four tests in tests/it/default_size.rs; two fail without the writer change.

Co-authored-by: narain1 <narain1@users.noreply.github.com>

* #3994: watermark font names round-trip through the textpath's CSS style (#4071)

* #3994: watermark font names round-trip through the textpath's CSS style

The writer deleted ", ', ; and : from a watermark's font name, so a name
like "Bob's Serif" was saved under a different name, a trailing backslash
escaped the style's closing quote (the font lost its size), and a control
character made the CSS invalid. The four characters a quoted CSS string
can't carry raw are now CSS escapes (\22, \3b, \3a, \5c, each with its
terminating space), control characters are dropped, and the apostrophe —
legal inside a double-quoted string — is kept. Both textpath font-family
readers map the escapes back, so the name round-trips.

Co-Authored-By: GLM-5.3 <noreply@z.ai>

* #3994: a font name's own apostrophes survive the textpath reader

Both textpath font-family readers trimmed every quote and apostrophe off
the ends of the value, so a font named 'Serif' (or Trailing') came back
without them. The reader now removes only the one pair of quotes around
the value; the round-trip test covers both cases.

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

---------

Co-authored-by: GLM-5.3 <noreply@z.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the direction of left, up and down block arrows (#3919) (#4022)

* Keep the direction of left, up and down block arrows (#3919)

* Cap and deduplicate the .aff TRY alphabet at parse time (#3981)

Fixes #3654

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Render: a dimmed header's borders keep one tone where they meet (#2946) (#3725)

#2947 fixed the fade by drawing a dimmed layer inside a single opacity layer,
but left no regression test for the tone: where a header table's borders cross,
a crossed or shared rule would quietly come out darker than a rule that is not
drawn on top of anything, and the undimmed view and the PDF export would not
agree with the screen.

The test renders the same page with the header dimmed and undimmed and checks
three rules: the darkest pixel at a crossing of a cell's top and left rules,
the darkest pixel on a border two neighbouring cells share, and one on an
outer border well clear of both. With the fade done as one layer, the
crossing and the shared border keep the tone of an isolated rule; measured
against a clean full-strength render, the dimmed tone is 0.45 of the
background, not 0.45 squared.

* Refuse arrange.group when members exceed the group child cap (#3958)

Fixes #3679

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Ignore docx chart number-format codes longer than 255 bytes (#3963)

Fixes #3729

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Format Painter: show the copy pointer over the text while armed (#3453) (#3560)

* Format Painter: show the copy pointer over the text while armed (#3453)

The canvas chose the I-beam over text whether or not the Format Painter
held copied formatting, so nothing showed the tool was waiting for a
click. While it is armed, the pointer over the text is now the copy
pointer (egui has no paintbrush), the same choice as the open DeckCraft
fix for the same report (storytold/deckcraft#846). Pictures, table
handles and resize edges keep their own pointers.

* Format Painter: the selection bar keeps its arrow while the painter is armed

Review follow-up: the bar's arrow shows that a click there selects whole
lines, so it outranks the painter's copy pointer (#3453).

* Format Painter pointer test: the selection bar keeps its arrow while armed

Review follow-up (#3453): the test now also hovers the selection bar with
the painter armed, so a precedence regression there fails it.

* Canvas: document show()

Review follow-up (#3453): CodeRabbit's docstring check counts the
function the pointer change sits in.

* #3453: Format Painter pointer test requires the I-beam once the painter is off

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

* #3453: Format Painter pointer test expects the hidden pointer where the I-beam is drawn

On Windows the canvas hides the system I-beam and draws its own (#1746), so
the pointer over the text reads None there, not Text.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Ruler: dragging a margin edge leaves the view where it is (#3900)

Each step of a margin drag on a ruler runs layout.margins, and like any
command that brought the caret back into view. With the caret off screen
(dragging the bottom margin while the text being edited is near the top
of the page) the view scrolled mid-drag: the page slid under the pointer,
the pointer's distance from the page's top changed with it, and the margin
ran far past where it was dropped. A drag of about 1 cm on the vertical
ruler set a 2 cm bottom margin to 8.1 cm.

The margin handles now run the command with run_keep_view, as table border
drags do (#1738).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Save, export and print keep the view where it is (#3895)

* Save, export and print keep the view where it is (fixes #3864)

* File › Options keeps the view where it is too

Changing a setting such as spell checking moves neither the caret nor
the text, so it no longer scrolls back to the caret.

* Layout: an empty paragraph with a hidden mark shows nothing, not even its list number (#3982) (#3992)

* Layout: an empty paragraph with a hidden mark shows nothing, not even its list number (#3982)

A paragraph whose mark is hidden and whose text is empty or hidden too is
now laid out as nothing, like a paragraph deleted in the final text: no
room, no list number and no shading. Its number is still counted, as in
Word ("1. Alpha", hidden item, "3. Beta"). Before, the body gave it no
room but drew its number under the next item's, and a table cell gave it
a line of its own with the number.

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the call…

---------

Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>
Co-authored-by: Mat Strange <54028700+matstrange@users.noreply.github.com>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: dfgodvan <dfgodvan@gmail.com>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Michael Higgins <55243596+Higgs32584@users.noreply.github.com>
Co-authored-by: In Mean <6434741+inmean@users.noreply.github.com>
Co-authored-by: Jayesh Prajapati <156836276+Jayesh-Prajapati9@users.noreply.github.com>
Co-authored-by: Gabriel Matozo <69991354+GabrielMatozo@users.noreply.github.com>
Co-authored-by: Mathieu Rancourt <40959576+MatRanc@users.noreply.github.com>
Co-authored-by: pakiat <49967840+pakiat@users.noreply.github.com>
Co-authored-by: Menonro <nitayd2001@gmail.com>
Co-authored-by: Gabriel Luís da Silva <79342117+mercuryVM@users.noreply.github.com>
Co-authored-by: dhruvLearner8 <83899022+dhruvLearner8@users.noreply.github.com>
Co-authored-by: vioreltus <142961713+vioreltus@users.noreply.github.com>
Co-authored-by: Dominik Strebel <dominik.strebel@gmail.com>
Co-authored-by: Dmitriy <dmitriy@mrzv.org>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: Dmitrii Khlapov <dkhlapov@pm.me>
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: VoiceLessQ <44067308+VoiceLessQ@users.noreply.github.com>
Co-authored-by: SlavenDj <53652906+SlavenDj@users.noreply.github.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
Co-authored-by: naxvog <naxvog@users.noreply.github.com>
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Federico Olivares <119693761+FedeOlivares@users.noreply.github.com>
Co-authored-by: Federico Olivares <FedeOlivares@users.noreply.github.com>
Co-authored-by: rabis <340973947+rabis7575@users.noreply.github.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
Co-authored-by: Narain <narainp1@outlook.com>
Co-authored-by: narain1 <narain1@users.noreply.github.com>
Co-authored-by: Capri <96988330+SusieeTheLinuxUser@users.noreply.github.com>
Co-authored-by: GLM-5.3 <noreply@z.ai>
2026-10-24 11:13:19 -04:00
+35 831b86dab3 Outline: re-match collapsed headings only after edits, via a hash index (#4014)
* Outline: re-match collapsed headings only after edits, via a hash index

Fixes #3621

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

* Write a text-box story shared by many shapes only once on docx save (#3983)

Fixes #3653

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Keep pending formatting revisions when Style Inspector clears formatting (#3971)

Fixes #3671

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the headless ui_requests queue and keep one Paste Special request (#3997)

Fixes #3641

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Validate PNG extension in file.exportPng (#3837)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* changes to filter dialogue

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-au…

* Khmer: ignore w14:ligatures when shaping Khmer text (#3921)

Documents that Word saves with w14:ligatures="historicalDiscretional" (every Khmer run
in typical Khmer OS files) turned liga and clig off. Khmer OS fonts build their
syllables there, so pre-base vowels and stacked signs came out in the wrong order
(ស៊ើបអង្កេត drawn as សីបអង្កតេ). Khmer text is now shaped with the font's defaults,
as Word does.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Skip quoted attribute values when scanning settings.xml tags (#3954)

Fixes #3682

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Rename kept docx parts whose names are not valid OPC part names (#3947)

* Rename kept docx parts whose names are not valid OPC part names

Fixes #3691

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its re…

* #3637: A table cell span from a .json file no longer overflows (#3927)

Insert Column, Merge Cells and Delete Column did arithmetic on the raw
CellProps::span, which a .json document can set to any u32, so a span
near u32::MAX overflowed (a panic in debug, a wrapped span in release).
They now work from the clamped span() and cap the result at MAX_COLS.

* #3663: "NaN" in the Font dialog's Size box no longer closes the app (#3928)

* #3663: "NaN" in the Font dialog's Size box no longer closes the app

"NaN" parses as an f32 and clamp keeps NaN, so the preview font size was
NaN and epaint's glyph assertion panicked while painting the dialog. The
preview now drops non-finite sizes and shows 12 pt.

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrel…

* Load ui.json with many unknown keys in linear time (#3968)

* Load ui.json with many unknown keys in linear time

Fixes #3659

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so…

* docbin: mark the format-mandated SHA-1 as reviewed (SonarCloud S4790) (#4020)

MS-OFFCRYPTO §2.3.5 derives the RC4 CryptoAPI key with SHA-1, so the
digest cannot be strengthened without breaking Word-written .doc files.
The NOSONAR marker records that this use is required by the format.

* Cap and deduplicate the .aff TRY alphabet at parse time (#3981)

Fixes #3654

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Render: a dimmed header's borders keep one tone where they meet (#2946) (#3725)

#2947 fixed the fade by drawing a dimmed layer inside a single opacity layer,
but left no regression test for the tone: where a header table's borders cross,
a crossed or shared rule would quietly come out darker than a rule that is not
drawn on top of anything, and the undimmed view and the PDF export would not
agree with the screen.

The test renders the same page with the header dimmed and undimmed and checks
three rules: the darkest pixel at a crossing of a cell's top and left rules,
the darkest pixel on a border two neighbouring cells share, and one on an
outer border well clear of both. With the fade done as one layer, the
crossing and the shared border keep the tone of an isolated rule; measured
against a clean full-strength render, the dimmed tone is 0.45 of the
background, not 0.45 squared.

* Refuse arrange.group when members exceed the group child cap (#3958)

Fixes #3679

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Ignore docx chart number-format codes longer than 255 bytes (#3963)

Fixes #3729

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Format Painter: show the copy pointer over the text while armed (#3453) (#3560)

* Format Painter: show the copy pointer over the text while armed (#3453)

The canvas chose the I-beam over text whether or not the Format Painter
held copied formatting, so nothing showed the tool was waiting for a
click. While it is armed, the pointer over the text is now the copy
pointer (egui has no paintbrush), the same choice as the open DeckCraft
fix for the same report (storytold/deckcraft#846). Pictures, table
handles and resize edges keep their own pointers.

* Format Painter: the selection bar keeps its arrow while the painter is armed

Review follow-up: the bar's arrow shows that a click there selects whole
lines, so it outranks the painter's copy pointer (#3453).

* Format Painter pointer test: the selection bar keeps its arrow while armed

Review follow-up (#3453): the test now also hovers the selection bar with
the painter armed, so a precedence regression there fails it.

* Canvas: document show()

Review follow-up (#3453): CodeRabbit's docstring check counts the
function the pointer change sits in.

* #3453: Format Painter pointer test requires the I-beam once the painter is off

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

* #3453: Format Painter pointer test expects the hidden pointer where the I-beam is drawn

On Windows the canvas hides the system I-beam and draws its own (#1746), so
the pointer over the text reads None there, not Text.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Ruler: dragging a margin edge leaves the view where it is (#3900)

Each step of a margin drag on a ruler runs layout.margins, and like any
command that brought the caret back into view. With the caret off screen
(dragging the bottom margin while the text being edited is near the top
of the page) the view scrolled mid-drag: the page slid under the pointer,
the pointer's distance from the page's top changed with it, and the margin
ran far past where it was dropped. A drag of about 1 cm on the vertical
ruler set a 2 cm bottom margin to 8.1 cm.

The margin handles now run the command with run_keep_view, as table border
drags do (#1738).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Save, export and print keep the view where it is (#3895)

* Save, export and print keep the view where it is (fixes #3864)

* File › Options keeps the view where it is too

Changing a setting such as spell checking moves neither the caret nor
the text, so it no longer scrolls back to the caret.

* Layout: an empty paragraph with a hidden mark shows nothing, not even its list number (#3982) (#3992)

* Layout: an empty paragraph with a hidden mark shows nothing, not even its list number (#3982)

A paragraph whose mark is hidden and whose text is empty or hidden too is
now laid out as nothing, like a paragraph deleted in the final text: no
room, no list number and no shading. Its number is still counted, as in
Word ("1. Alpha", hidden item, "3. Beta"). Before, the body gave it no
room but drew its number under the next item's, and a table cell gave it
a line of its own with the number.

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouch…

* Tables: a row fits on a page only with room for the border below it (#3814) (#3815)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Tables: a row fits on a page only with room for the border below it

Word places a table row on a page only when the border under it fits too:
that border belongs to the next row's top band, but the row is moved on when
the page has its height and not the border. WordCraft checked the row's height
alone, so a page could hold one row more than Word's.

RowLayout::edge is the width of the bottom border of a row that another row
follows; place_table counts it when it checks whether a row fits, and so does
the check that keeps repeated header rows from staying alone at the foot of a
page. The part of a split row left on the page has none (it ends the page).

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Autho…

* #3734: ui.resize refuses sizes that aren't positive and clamps to the GPU's limit (#3920)

* #3734: ui.resize refuses sizes that aren't positive and clamps to the GPU's limit

The control channel forwarded any width/height to the viewport. Zero, negative and huge sizes now get an error or a size the window can have: the app's minimum and the largest texture the GPU draws (at most 16384 points). The reply carries the size asked for.

* #3734: refuse non-finite sizes, and let the GPU limit win over the minimum

Review follow-up: an infinite side (1e300 once read as f32) is refused rather than clamped, and a GPU limit below the app's minimum caps the size instead of being raised to it.

* Backstage: non-Latin file names in Recent and Recover draw, not boxes (#3545)

File › Home's Recent list and File › Recover draw file names with the
interface faces, which have no CJK, Arabic, Hebrew… glyphs; nothing asked
for a covering face the way the window caption and comment balloons do, so
a name like 小红书帖子-草稿.md showed as a row of boxes. A new
ensure_backstage_fonts does what ensure_caption_fonts does for those names:
CJK loads the CJK UI font, other scripts borrow the document fallback faces,
re-checked only when the names change.

Regression test fails without the call; checked live on the Recover page.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Print and PDF export use the Numeral setting (#3784) (#3824)

* Fix .doc bookmark table desync on names longer than 40 characters (#3949)

* Fix .doc bookmark table desync on names longer than 40 characters

Fixes #3690

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a re…

* Status bar names the page in view, not only the caret's page (#3880) (#3901)

* Inline shared Word 2003 headers, notes and comments once (#3985)

Fixes #3652

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* G14: Romanian: translate the 68 rows that carried English text; feminine stângă/dreaptă, unambiguous arrows and brackets (#3558)

The integration of #3203 filled the keys the catalog lacked with English text.
These are now translated (cm, mm and pi stay as they are). 24 labels take the
feminine stângă/dreaptă (Bordură stângă, Margine dreaptă), arrows read
"spre stânga/dreapta" and brackets say which bracket, so "paranteză dreaptă"
(square bracket) never meets dreapta (right).


Claude-Session: https://claude.ai/code/session_01JadgGo4G2GHaAMNb3qyygk

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Keep inline content and formatting in Convert Text to Table (#3937)

Fixes #3751

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Compare: don't let a character-level diff scramble a reordered digit run (#3224)

lcs_ops is a textbook LCS, which finds the mathematically longest common
subsequence, not the most readable edit script. On two short strings that
happen to share a few characters in a different order (very common with
dates, IDs, phone numbers), it faithfully threads a chain of single-character
matches through what a reader expects to be one clean replacement — e.g.
"01.09.2026" -> "19.10.2026" came out as an alternating run of tiny
deletions and insertions instead of deleting "01.09" and inserting "19.10".

Add collapse_noise_matches, called only for character-level compares: it
folds a short (length-1) match that sits strictly inside a run of edits back
into the surrounding delete/insert, flattening the region into one clean
delete followed by one clean insert. A match that opens or closes the whole
sequence is the genuine common prefix/suffix lcs_ops already stripped before
running its table, so it's never touched regardless of length, and the fold
is gated on character granularity only — a matched whole word at word-level
compare is essentially never a coincidence and must survive.

Fixes #3094.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* Regression tests for #2 and #504 (#3860)

* #2: test that style gallery labels are truncated and hanging previews stay in view

Long custom style names were painted at full width and ran into neighbouring tiles, and a style with a hanging indent larger than its left indent previewed with its first line off the tile's left edge. The tests render the Styles gallery with one such style: the label must be no wider than its 74-point tile and elided, and the preview's text must start at the tile's left edge and stay whole. Without the fix the label measures 291 points and the preview's ink spans only columns 0..24 (cut by the edge).

* #504: test that the Backstage side menu edge is not a resize handle

The File side menu is a fixed 200-point panel, but egui left it resizable, so hovering its right edge drew a resize line and showed a resize cursor that did nothing. The test hovers the edge and checks the pointer is not a resize cursor. Without the fix it reports ResizeEast.

* Restrict Editing: read only and comments-only protection grey out edits (#3264)

* Restrict Editing: read only and comments-only protection grey out edits

Without an exception the author may edit, a read-only or comments-only
document refuses every edit, but the ribbon drew the buttons live.
greyed_out now covers every refusal that doesn't depend on params or the
selection (read only, comments only, forms without fillable controls,
Accept/Reject under tracked-changes protection), and check_before refuses
through it. The ribbon reads the facts that decide it (exceptions for the
author, fillable controls) from a cache keyed by document, revision and
author; the engine's own check computes them fresh.

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

* Restrict Editing: protection refuses changing whether moves are tracked

Track Changes Options stays available under protection (its options are the
user's own), but trackMoves edits the document's setting, so it passes the
same check as an edit and isn't limited to an exception.

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that …

* Font: a selected text box's Font commands format all its text (#3898)

* Font: a selected text box's Font commands format all its text

With a text box selected as an object (its frame, not text in it), Word's
Font group formats all the box's text. WordCraft formatted only the box's
anchor character in the paragraph around it, so the box's text kept its
font. Select All doesn't reach a floating box's text either (as in Word),
which left no way to change a box's font short of editing inside it, and
none when the box sits behind text, where a click goes to the text over it.

format::apply and the toggles' reading of the current formatting now take
a selected text box's whole story, as the WordArt commands already do.

* Contributors: pakiat's display name

* Font: Change Case, Clear and Reset on a selected text box work on its text

With a text box selected as an object, Word's Change Case, Clear All
Formatting and Reset Character Formatting work on the box's text and leave
the body text around it alone (observed black-box). format.changeCase,
format.clear and format.resetChar used the box's anchor in the body
instead, so Clear could reset the surrounding paragraph while the box kept
its formatting.

They now take a selected text box's whole story, as the other Font
commands do since the previous commit. An empty selected box also reads
its paragraph mark for the toggles' current state (it read the anchor's
run around it), so Bold on an empty box in bold body text turns Bold on.

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

* Font: a selected text box shows its own paragraph style

With a text box selected as an object, format.state reported the box's
character formatting but the style of the body paragraph the box is
anchored in. Word shows the box's own style then (observed black-box: a
Heading 1 box in a Normal paragraph shows Heading 1). The style and its
name now come from the box's first paragraph.

Also doc comments for format.changeCase and format.clear.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* packaging: use [[ for bash conditional tests (SonarCloud S7688) (#4024)

SonarCloud flags every bare [ conditional in bash scripts under packaging/ as a reliability smell (rule S7688). Convert the flagged [ ... ] tests to [[ ... ]]: safer word-splitting and no glob surprises, same semantics for these string and file tests.

* A document with many relationships takes minutes to open (#3809)

* The reader takes the referenced relationship ids from the markup's attributes

Opening a document looked for each relationship's id in the main document's
part, to know which parts the markup already refers to so they aren't
carried over twice. That was a full pass over the part, once per
relationship: the open cost was the part's size times the number of
relationships, where parsing the same part costs its size once. With the
reader's limits — a 256 MiB part and 1024 relationships — that is about
256 GiB of byte comparisons for one document.

The ids the main document refers to are the ones it names in a relationship
attribute (`r:id`, VML's `o:relid`) — the same rule the embedded objects
use. The reader has already parsed the main document, so the ids are taken
from that tree: one pass over the markup it has, no search over the part at
all, and no second parse. The set of ids is bounded by the relationships the
reader looks at, so a file with a large relationship part does not make the
open allocate for ids no lookup can reach.

An id that only appears as text is not a reference. The text names no part,
and treating it as one lost the part on save: the relationship was skipped
as "already referred to", so its target was carried neither by the element
that names it nor as an unmodelled part.

On a 64 MiB main part, opening a document with 1024 relationships went from
224 seconds to 0.20 — and the file is 70 KB on disk, so it was a way to make
the app spend minutes of CPU on a small input.

#3411

* Record the relationship-scan fix in the progress docs

The file-format parity document says how the ids are collected and what the
open costs before and after; the ranked gap list holds one row per gap now,
and its #20 row carries the measured figure. The progress log and the
revision history gain the entry.

#3411

* #3411: read a relationship the markup names without the prefix

The reader resolves a relationship id through `El::attr`, which accepts an
unqualified attribute of the same local name (a producer that left the
prefix out writes `id="rId5"`). The relationship-id handling did not: it
counted only the attributes in the relationship namespace and VML's
`o:relid`, so an id written that way named no part.

One context-aware helper now decides what a relationship attribute is, for
the reader and the writer alike (`xml::is_rel_attr`): the relationship
namespace and `o:relid` always are, an unqualified attribute is one on an
element that carries a relationship and only with the local name of a
relationship attribute, and an ordinary `id` — a VML shape's, a drawing's
`wp:docPr`, a picture's `pic:cNvPr`, a page background's — is the
element's own and names no part.

With it, an OLE object's picture whose image is named by an unqualified
`id` keeps its image: the part is kept with the object, and the writer
keeps the attribute and rewrites it with the id the new package holds
the image under. Before, the image was dropped and the written object
pointed at a relationship that was not there.

Tests: `docx::read::embed::tests` (a hyperlink's unqualified `id` names
the part, a VML shape's does not), `docx::xml::tests` (the helper),
`docx::tests::fixtures` (the round trip: the attribute is preserved and
rewritten to the id the new package gives the part).

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live pr…

* A word with no break opportunity in it makes the layout quadratic (#3808)

* A word with no break opportunity in it no longer makes the layout quadratic

Laying out a line beside a floating object measures the word that starts on
it, to tell whether the word fits the full line. The measurement walked to
the end of the paragraph every time, so a word with no break opportunity in
it — which takes a line per few characters — walked the paragraph once per
line, and the layout was quadratic in the word's length.

The measurement now stops once the word is clearly wider than the room there
is: the answer is known by then, and the advances are widths, so the rest of
the word could only make it wider — unless a font pulls a glyph back with a
negative advance, and then the measurement carries on only as far as the rest
of the word could take the width back.

The same answer, the same layout, one line's worth of clusters per line
instead of a paragraph's. A 1 MiB word went from 29.42 s to 0.35 s, and a
4 MiB one from 472.34 s to 1.38 s.

#3412

* Record the layout fix in the progress docs

The layout parity table gains the measurement for a word with no break
opportunity in it laid out beside a floating object, and a note says what
the fix was. The ranked gap list holds one row per gap now, and its #20 row
carries the measured figure. The progress log and the revision history gain
the entry.

#3412

* Measure every word once, before the line is broken (#3412)

The measurement of the word that starts a line walked the word to find out how
wide it was, and a font that pulls a glyph back with a negative advance keeps
the walk going: the bound that stopped it early was the sum of the advances the
rest of the word could give back, and when the advances alternate in sign that
sum stays so negative that the walk never stops early.

Every word is measured once now, before the line is broken: the width of a word
is the sum of its advances, the negative ones included, and asking whether the
word fits the full line is a lookup rather than another pass over it.

The answer is the same: the sum is taken over the same clusters the scan took
them over. It is added up from the word's end rather than its start, which is
the same number in all but the last bits of a float — the sum a float holds
depends on the order it is added in, and the two differ by far less than the
tenth of a point the comparison that uses the width allows.

The ranked-gap rows that were not this fix's are back to how main has them.

* Lay the negative-advance word out beside a float (#3412)

The width of a word is checked against the width of a word, and a font that
pulls a glyph back takes the width off the rest of the word as well. The
layout now does that too: the word is laid out beside a floating object, in
the strip the object leaves, and it fits there — where the same word without
the glyph pulled back does not, and moves on whole below the object instead.

The strip the object leaves is half way between the two widths, so the test
does not depend on the font's metrics.

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the ne…

* doc: preserve rotated free endpoints when rerouting connectors (#3882)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* doc: preserve rotated free endpoints when rerouting connectors

Transform fallback connector endpoints through the existing rotation and flips around the frame centre before rerouting clears rotation. Attached endpoints continue resolving from target sites; free or unresolved endpoints retain their visible positions.

Keep current shape outline behavior and cover connector kinds, either endpoint, rotations and flips, unresolved targets, invalid sites and settled rerouting. Document the model-level correction without changing parity estimates.

Model: openai-codex/gpt-6.1-sol

* fix(connectors): preserve displayed endpoints in scaled groups

Pass the whole group into reroute_group so fallback endpoints use the same scaled and capped child rectangle as rendering. Apply child spin after group scaling, then inverse-map displayed endpoints to group coordinates before routing. Keep resolved connection-site routing unchanged and migrate the engine caller.

Avoid rewriting fallback connectors when degenerate scales or the display-size cap prevent representing their displayed geometry. Add regressions for nonuniform scaling, rotations, flips, unresolved sites, and capped reroutes, and update the existing connector documentation.

Model: openai-codex/gpt-6.1-sol

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <norepl…

* Selection bar: right margin for right-to-left text, and beside table rows (UI gap) (#2042)

* gap: selection bar RTL/table rows WIP

* gap: selection bar in the right margin for right-to-left paragraphs and beside table rows

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

* gap: selection bar review fixes (row bar only beside the table, Shift+click from outside, text-box tables)

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

* gap: selection bar review round 2 (0.5pt row-overlap tolerance, body-story check for Shift+click)

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

* gap: selection bar: triple-click or Ctrl+click beside a table row selects the document, as in Word

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

* gap: selection bar: ui-parity wording (only triple-click was checked in Word)

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

* gap: selection bar: fix docs after rebase onto main

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

* gap: selection bar: a Shift+click drag keeps the anchor row of its press

table.selectAt takes an optional anchorRow; the app reads the anchor at the
press and sends it on every drag frame, so pointer jitter after an upward
Shift+click no longer re-reads it from the rows the press selected.

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

* Selection bar RTL test: the left margin shows no bar arrow (Windows reports no cursor there)

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

* #2042: repair three doc status lines a #3882 merge left with a stray Target

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>

* Mail merge: go to any record, and merge or print a range of records (#3028) (#3894)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* Mail merge: go to any record, and merge or print a range of records (#3028)

Preview Results gets First Record and Last Record buttons and a record
number box; a number typed there and Enter preview that record.

Finish & Merge is now a menu. Edit Individual Documents... and Print
Documents... ask for all records, the current record, or a From and To
range by record number, and Print Documents opens Print for the merged
document. The dialog checks the range before Save Changes asks about
unsaved work.

New commands mailings.first, mailings.last, mailings.goToRecord and
mailings.printDocuments; mailings.finish takes "records": all|current.
A range with no records in it is refused instead of replacing the
document with an empty one.

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-A…

* Translate ribbon disabled-reason tooltips (#2678) (#2680)

* #1086: ribbon tooltips show why a control is disabled

Hover hints give the command's own reason, cover split-button arrows,
menu buttons and the font/size/outline boxes, and show on keyboard focus.

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

* #2678: translate disabled-control reasons in every interface language

Ribbon hints show a command's reason for being off; the 28 reasons now
have entries in all 22 catalogs (machine-written), and an audit test
reads every enabled check from the engine's command sources and fails
when a catalog lacks one.

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

* #2678: translate the two disabled reasons added on main

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

* #2678: drop the unused combo import left by the merge

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

* #2678: translate the new disabled reasons and the da/nl/ro/sl catalogs; audit skips match patterns

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

* #2678: translate the no-document hint in every catalog and audit it; ROADMAP states the audit's real coverage

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

* Mail merge: go to any record, and merge or print a range of records (#3028) (#3894)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* Mail merge: go to any record, and merge or print a range of records (#3028)

Preview Results gets First Record and Last Record buttons and a record
number box; a number typed there and Enter preview that record.

Finish & Merge is now a menu. Edit Individual Documents... and Print
Documents... ask for all records, the current record, or a From and To
range by record number, and Print Documents opens Print for the merged
document. The dialog checks the range before Save Changes asks about
unsaved work.

New commands mailings.first, mailings.last, mailings.goToRecord and
mailings.printDocuments; mailings.finish takes "records": all|current.
A range with no records in it is refused instead of replacing the
document with an empty one.

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode…

* CI: one test binary per crate, nextest, line tables only (#4025)

* CI: line-table debug info for dev builds, none for dependencies

* CI: one integration-test binary per crate

* CI: nextest with doctests, test build without examples, one wasm check

* CI: the interface audit reports missing catalog entries instead of failing

* Docs: the interface audit, not CI as a whole, passes with missing catalog entries

* CI: run the gate through craft-common-actions' shared gate.yml; FreeBSD nightly and on release (#3913)

ci.yml calls the shared workflow (cached apt packages, nextest, main-only cache saves, Cargo.lock check). FreeBSD runs nightly and on pushes to release instead of on pull requests. Depends on storytold/craft-common-actions#69.

* i18n tests: missing translations never fail; one report lists them (#4043)

* CI: line-table debug info for dev builds, none for dependencies

* CI: one integration-test binary per crate

* CI: nextest with doctests, test build without examples, one wasm check

* CI: the interface audit reports missing catalog entries instead of failing

* i18n tests: the same checks for every language, none fail on a missing translation

* i18n tests: mechanism tests use fixtures, not catalog text

* Docs: the interface audit, not CI as a whole, passes with missing catalog entries

* i18n tests: disabled reasons a catalog lacks are reported, not a failure

* i18n tests: one missing-translation report, printed by cargo xtask ci

* i18n docs: the missing-translation report

* xtask: the i18n report reuses the test run's build

* ci(sonar): don't cancel superseded runs on main, so coverage is published (#4070)

* Packaging: pin the signing client and appimagetool, and check their hashes (#3326)

* Packaging: pin the Windows signing client and check its hash

* Packaging: pin appimagetool and check its hash

* Packaging: pin the AppImage runtime and stop clearly when an architecture has no pinned tool

* Packaging: empty hashes are quoted

* Change Case follows each run's language for Turkic casing (#3479) (#3480)

Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>

* build(i18n): embed the craft-i18n catalogs pinned in craft-i18n.rev

Adds the craft-i18n crate (craft-libs f7cc8a7, storytold/craft-libs#247)
and the build.rs recipe from craft-i18n's docs/integration.md: with
CRAFT_I18N_DIR set, CRAFT_I18N embeds each locale's common.tsv and
wordcraft.tsv (paths via {:?}, never canonicalized); unset, it is empty
and the build is English only; CRAFT_I18N_REQUIRED=1 makes that an
error. craft-i18n.rev pins storytold/craft-i18n be1f053 (LF via
.gitattributes); the flatpak manifest checks out the same commit and
sets CRAFT_I18N_DIR.

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

* feat(i18n): look interface strings up through the craft-i18n crate

The i18n module becomes a thin wrapper over craft_i18n::I18n built from
the embedded catalogs, offering exactly LANGUAGES (slimmed to code,
prefer_hans, editing_lang; names come from @id language.name).
WORDCRAFT_LANGUAGE and the sys-locale read stay; non-Brazilian pt-* stays
English before the crate's matching (test). The pseudo-language uses
the crate's Lang::PSEUDO and looked_up(). The Design tab's "Open"
spacing preset uses get("spacing", "Open") so it never shows the file
verb. The missing-translation report adds check_all() problems and a
kept-English bucket and never fails; the untranslated-text audit still
fails on text that skips tl!. Catalog-dependent tests skip with a note
when nothing was embedded; a test keeps the flatpak commit equal to
craft-i18n.rev.

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

* chore(i18n): remove the in-repo catalogs and parser

crates/ui-egui/src/i18n/*.tsv and catalog.rs are gone: translations live
in storytold/craft-i18n (locales/<l>/wordcraft.tsv, synced at
wordcraft@806a945) and are credited in its ATTRIBUTION.md. To carry an
open PR's catalog rows over, from a craft-i18n checkout:

  tools/sync_app.py ../wordcraft 806a945d85 --ref <branch>

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

* Options › Language names the proofing language at the caret (#3150) (#3177)

The note under "Proofing language" always said English (United States). It now names
the language at the caret, as the status bar does, and the 22 catalogs carry the new sentence.

Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>

* Proofing options reach the Czech, Croatian, Indonesian, Italian, Polish and Portuguese checks (#3966)

Each of these languages accepted every word in capitals of up to six letters and every word
with a digit on its own, so turning off Ignore words in UPPERCASE or Ignore words that contain
numbers had no effect on them for short words. The check now takes the options, as the Spanish
one does; the direct is_correct/is_correct_with keep the defaults.

Co-authored-by: Federico Olivares <FedeOlivares@users.noreply.github.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>

* PDF: pin craft-libs 9f75d3dc for the PNG and PDF input bounds (#4084)

* #1365: PDF: pin craft-libs main, whose Krilla leaves out simple-text (no rustybuzz, ttf-parser)

craft-libs 8f5a2831 turns Krilla's default features off in the shared
workspace and keeps raster-images, so simple-text (the only path to the
unmaintained rustybuzz and ttf-parser) is no longer built. WordCraft
shapes text itself and hands Krilla positioned glyphs. The vendored Krilla
and craft-pdf's writer are unchanged between the two revisions.

Cargo.lock loses rustybuzz, ttf-parser, core_maths,
unicode-bidi-mirroring, unicode-ccc and unicode-properties.

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

* #1365: docs/shared-pdf.md: rewrap the backend features paragraph

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

* PDF: pin craft-libs main, which bounds decoded PNG images in Krilla

* PDF: pin craft-libs 9f75d3dc, with the PDF input bounds of craft-libs#254

---------

Co-authored-by: rabis <340973947+rabis7575@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* chore: resolve SonarCloud maintainability issues (#4083)

* chore: resolve SonarCloud maintainability issues

* fix: restore compiling refactors, revert behaviour changes, run cargo fmt

- fontdb::norm: keep the closure for filter (char::is_alphanumeric takes
  self, not &self, so it does not fit Iterator::filter).
- story.rs: write_mark_rpr needs &mut self (rev_attrs, rpr_change).
- Revert the viewport meta change and the return 0 added to
  workspace_version/sha256, which changed behaviour or masked failures.
- cargo fmt.

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

* Import Document in odt page_parts after dropping glob re-export

* fix: apply_kind_properties binds ContentControl; cell_ref keeps char closures

* fix(engine): pass case helpers by value (clippy needless borrows)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>

* Sonar S1612: use fn paths for simple method closures (#4103)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Sonar S1612: pass PoisonError::into_inner instead of a closure (#4102)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Sonar S7682: explicit return in env.sh helpers (#4105)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Format negative zero as 0 in spreadsheet import (#4040)

Found while testing crates/formats/src/sheet.rs:726-727 (fn number)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Ignore quoted text when detecting recipient CSV delimiter (#4125)

Found while testing crates/engine/src/cmd/mailings.rs:526-527 (parse_csv)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap label and envelope text length in mailings commands (#4008)

Fixes #3633

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* DOCX: a hinted East Asian or complex-script font no longer replaces the Latin font (fixes #4031) (#4067)

* Phonetic Guide dialog puts the keyboard in the Ruby text box on open (#3914) (#4021)

* Route ui.screenshot path writes through the guarded engine writer (#4048)

Fixes #3635

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Dialogs: draw real tabs (Paragraph, Find, About) (#4029)

* Dialog tabs: draw real tabs in Paragraph, Find and About (#4026)

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

* A word with no break opportunity in it makes the layout quadratic (#3808)

* A word with no break opportunity in it no longer makes the layout quadratic

Laying out a line beside a floating object measures the word that starts on
it, to tell whether the word fits the full line. The measurement walked to
the end of the paragraph every time, so a word with no break opportunity in
it — which takes a line per few characters — walked the paragraph once per
line, and the layout was quadratic in the word's length.

The measurement now stops once the word is clearly wider than the room there
is: the answer is known by then, and the advances are widths, so the rest of
the word could only make it wider — unless a font pulls a glyph back with a
negative advance, and then the measurement carries on only as far as the rest
of the word could take the width back.

The same answer, the same layout, one line's worth of clusters per line
instead of a paragraph's. A 1 MiB word went from 29.42 s to 0.35 s, and a
4 MiB one from 472.34 s to 1.38 s.

#3412

* Record the layout fix in the progress docs

The layout parity table gains the measurement for a word with no break
opportunity in it laid out beside a floating object, and a note says what
the fix was. The ranked gap list holds one row per gap now, and its #20 row
carries the measured figure. The progress log and the revision history gain
the entry.

#3412

* Measure every word once, before the line is broken (#3412)

The measurement of the word that starts a line walked the word to find out how
wide it was, and a font that pulls a glyph back with a negative advance keeps
the walk going: the bound that stopped it early was the sum of the advances the
rest of the word could give back, and when the advances alternate in sign that
sum stays so negative that the walk never stops early.

Every word is measured once now, before the line is broken: the width of a word
is the sum of its advances, the negative ones included, and asking whether the
word fits the full line is a lookup rather than another pass over it.

The answer is the same: the sum is taken over the same clusters the scan took
them over. It is added up from the word's end rather than its start, which is
the same number in all but the last bits of a float — the sum a float holds
depends on the order it is added in, and the two differ by far less than the
tenth of a point the comparison that uses the width allows.

The ranked-gap rows that were not this fix's are back to how main has them.

* Lay the negative-advance word out beside a float (#3412)

The width of a word is checked against the width of a word, and a font that
pulls a glyph back takes the width off the rest of the word as well. The
layout now does that too: the word is laid out beside a floating object, in
the strip the object leaves, and it fits there — where the same word without
the glyph pulled back does not, and moves on whole below the object instead.

The strip the object leaves is half way between the two widths, so the test
does not depend on the font's metrics.

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document ei…

* #4014: drop the wall-clock assert from the collapsed-headings test (flaky on slow CI)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Michael Higgins <55243596+Higgs32584@users.noreply.github.com>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>
Co-authored-by: dfgodvan <dfgodvan@gmail.com>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: In Mean <6434741+inmean@users.noreply.github.com>
Co-authored-by: Jayesh Prajapati <156836276+Jayesh-Prajapati9@users.noreply.github.com>
Co-authored-by: Gabriel Matozo <69991354+GabrielMatozo@users.noreply.github.com>
Co-authored-by: Mathieu Rancourt <40959576+MatRanc@users.noreply.github.com>
Co-authored-by: pakiat <49967840+pakiat@users.noreply.github.com>
Co-authored-by: Marco Arnulfo <103679419+marcoarnulfo@users.noreply.github.com>
Co-authored-by: Menonro <nitayd2001@gmail.com>
Co-authored-by: Gabriel Luís da Silva <79342117+mercuryVM@users.noreply.github.com>
Co-authored-by: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com>
Co-authored-by: dhruvLearner8 <83899022+dhruvLearner8@users.noreply.github.com>
Co-authored-by: vioreltus <142961713+vioreltus@users.noreply.github.com>
Co-authored-by: Dominik Strebel <dominik.strebel@gmail.com>
Co-authored-by: Mat Strange <54028700+matstrange@users.noreply.github.com>
Co-authored-by: Dmitriy <dmitriy@mrzv.org>
Co-authored-by: Dmitrii Khlapov <dkhlapov@pm.me>
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: VoiceLessQ <44067308+VoiceLessQ@users.noreply.github.com>
Co-authored-by: SlavenDj <53652906+SlavenDj@users.noreply.github.com>
Co-authored-by: Martin Ball <martin.ball@ucscaregroup.co.uk>
Co-authored-by: naxvog <naxvog@users.noreply.github.com>
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Federico Olivares <119693761+FedeOlivares@users.noreply.github.com>
Co-authored-by: Federico Olivares <FedeOlivares@users.noreply.github.com>
Co-authored-by: rabis <340973947+rabis7575@users.noreply.github.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
2026-10-24 10:46:13 -04:00
NetterSilverandClaude Haiku 5.5 7c5f3846d9 Cap label and envelope text length in mailings commands (#4008)
Fixes #3633

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-24 10:11:12 -04:00
NetterSilverandClaude Haiku 5.5 74eb4b184f Ignore quoted text when detecting recipient CSV delimiter (#4125)
Found while testing crates/engine/src/cmd/mailings.rs:526-527 (parse_csv)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-24 10:07:58 -04:00
Dylan M. TaylorandClaude Sonnet 5.5 ba7e7293e4 Sonar S1612: pass PoisonError::into_inner instead of a closure (#4102)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 04:33:12 -04:00
Dylan M. TaylorandClaude Sonnet 5.5 01d0c63451 Sonar S1612: use fn paths for simple method closures (#4103)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 04:33:08 -04:00
47edcd2ff2 chore: resolve SonarCloud maintainability issues (#4083)
* chore: resolve SonarCloud maintainability issues

* fix: restore compiling refactors, revert behaviour changes, run cargo fmt

- fontdb::norm: keep the closure for filter (char::is_alphanumeric takes
  self, not &self, so it does not fit Iterator::filter).
- story.rs: write_mark_rpr needs &mut self (rev_attrs, rpr_change).
- Revert the viewport meta change and the return 0 added to
  workspace_version/sha256, which changed behaviour or masked failures.
- cargo fmt.

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

* Import Document in odt page_parts after dropping glob re-export

* fix: apply_kind_properties binds ContentControl; cell_ref keeps char closures

* fix(engine): pass case helpers by value (clippy needless borrows)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
2026-10-24 03:54:19 -04:00
Federico Olivares 707f02efae Merge origin/main into spanish-proofing
Upstream merged #3177 (dictionary hints for every Hunspell language) and #3480.
Resolve the conflicts in crates/proof/src/lib.rs by taking #3177's
dictionary_loaded/dictionary_needed structure and adding the four Spanish
varieties to it, and in docs/spelling-dictionaries.md keeping both sides.
2026-10-23 19:06:06 -03:00
SlavenDjandaiongg 3ed7fe977c Change Case follows each run's language for Turkic casing (#3479) (#3480)
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
2026-10-23 17:14:34 -04:00
Federico Olivares 02f037bdde Merge origin/main into spanish-proofing
Resolve the ROADMAP.md and docs/gaps.md status-line conflicts, keeping both
changes.
2026-10-23 14:37:36 -03:00
Federico Olivares f8d1d9135f Merge origin/main into spanish-proofing
Resolve the ROADMAP.md and docs/gaps.md conflicts: both status lines keep
both changes, and the Spanish proofing history row stays above main's.
2026-10-23 14:35:22 -03:00
+27 8919642b42 Mail merge: go to any record, and merge or print a range of records (#3028) (#3894)
* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* Mail merge: go to any record, and merge or print a range of records (#3028)

Preview Results gets First Record and Last Record buttons and a record
number box; a number typed there and Enter preview that record.

Finish & Merge is now a menu. Edit Individual Documents... and Print
Documents... ask for all records, the current record, or a From and To
range by record number, and Print Documents opens Print for the merged
document. The dialog checks the range before Save Changes asks about
unsaved work.

New commands mailings.first, mailings.last, mailings.goToRecord and
mailings.printDocuments; mailings.finish takes "records": all|current.
A range with no records in it is refused instead of replacing the
document with an empty one.

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* #3944: drop the wall-clock assert from the long-root-tag test

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

---------

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>
Co-authored-by: dfgodvan <dfgodvan@gmail.com>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>

* Write a text-box story shared by many shapes only once on docx save (#3983)

Fixes #3653

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Keep pending formatting revisions when Style Inspector clears formatting (#3971)

Fixes #3671

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the headless ui_requests queue and keep one Paste Special request (#3997)

Fixes #3641

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Validate PNG extension in file.exportPng (#3837)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* changes to filter dialogue

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-au…

* Khmer: ignore w14:ligatures when shaping Khmer text (#3921)

Documents that Word saves with w14:ligatures="historicalDiscretional" (every Khmer run
in typical Khmer OS files) turned liga and clig off. Khmer OS fonts build their
syllables there, so pre-base vowels and stacked signs came out in the wrong order
(ស៊ើបអង្កេត drawn as សីបអង្កតេ). Khmer text is now shaped with the font's defaults,
as Word does.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Skip quoted attribute values when scanning settings.xml tags (#3954)

Fixes #3682

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Rename kept docx parts whose names are not valid OPC part names (#3947)

* Rename kept docx parts whose names are not valid OPC part names

Fixes #3691

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its re…

* #3637: A table cell span from a .json file no longer overflows (#3927)

Insert Column, Merge Cells and Delete Column did arithmetic on the raw
CellProps::span, which a .json document can set to any u32, so a span
near u32::MAX overflowed (a panic in debug, a wrapped span in release).
They now work from the clamped span() and cap the result at MAX_COLS.

* #3663: "NaN" in the Font dialog's Size box no longer closes the app (#3928)

* #3663: "NaN" in the Font dialog's Size box no longer closes the app

"NaN" parses as an f32 and clamp keeps NaN, so the preview font size was
NaN and epaint's glyph assertion panicked while painting the dialog. The
preview now drops non-finite sizes and shows 12 pt.

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrel…

* Load ui.json with many unknown keys in linear time (#3968)

* Load ui.json with many unknown keys in linear time

Fixes #3659

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so…

* docbin: mark the format-mandated SHA-1 as reviewed (SonarCloud S4790) (#4020)

MS-OFFCRYPTO §2.3.5 derives the RC4 CryptoAPI key with SHA-1, so the
digest cannot be strengthened without breaking Word-written .doc files.
The NOSONAR marker records that this use is required by the format.

* Cap and deduplicate the .aff TRY alphabet at parse time (#3981)

Fixes #3654

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Render: a dimmed header's borders keep one tone where they meet (#2946) (#3725)

#2947 fixed the fade by drawing a dimmed layer inside a single opacity layer,
but left no regression test for the tone: where a header table's borders cross,
a crossed or shared rule would quietly come out darker than a rule that is not
drawn on top of anything, and the undimmed view and the PDF export would not
agree with the screen.

The test renders the same page with the header dimmed and undimmed and checks
three rules: the darkest pixel at a crossing of a cell's top and left rules,
the darkest pixel on a border two neighbouring cells share, and one on an
outer border well clear of both. With the fade done as one layer, the
crossing and the shared border keep the tone of an isolated rule; measured
against a clean full-strength render, the dimmed tone is 0.45 of the
background, not 0.45 squared.

* Refuse arrange.group when members exceed the group child cap (#3958)

Fixes #3679

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Ignore docx chart number-format codes longer than 255 bytes (#3963)

Fixes #3729

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Format Painter: show the copy pointer over the text while armed (#3453) (#3560)

* Format Painter: show the copy pointer over the text while armed (#3453)

The canvas chose the I-beam over text whether or not the Format Painter
held copied formatting, so nothing showed the tool was waiting for a
click. While it is armed, the pointer over the text is now the copy
pointer (egui has no paintbrush), the same choice as the open DeckCraft
fix for the same report (storytold/deckcraft#846). Pictures, table
handles and resize edges keep their own pointers.

* Format Painter: the selection bar keeps its arrow while the painter is armed

Review follow-up: the bar's arrow shows that a click there selects whole
lines, so it outranks the painter's copy pointer (#3453).

* Format Painter pointer test: the selection bar keeps its arrow while armed

Review follow-up (#3453): the test now also hovers the selection bar with
the painter armed, so a precedence regression there fails it.

* Canvas: document show()

Review follow-up (#3453): CodeRabbit's docstring check counts the
function the pointer change sits in.

* #3453: Format Painter pointer test requires the I-beam once the painter is off

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

* #3453: Format Painter pointer test expects the hidden pointer where the I-beam is drawn

On Windows the canvas hides the system I-beam and draws its own (#1746), so
the pointer over the text reads None there, not Text.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Ruler: dragging a margin edge leaves the view where it is (#3900)

Each step of a margin drag on a ruler runs layout.margins, and like any
command that brought the caret back into view. With the caret off screen
(dragging the bottom margin while the text being edited is near the top
of the page) the view scrolled mid-drag: the page slid under the pointer,
the pointer's distance from the page's top changed with it, and the margin
ran far past where it was dropped. A drag of about 1 cm on the vertical
ruler set a 2 cm bottom margin to 8.1 cm.

The margin handles now run the command with run_keep_view, as table border
drags do (#1738).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Save, export and print keep the view where it is (#3895)

* Save, export and print keep the view where it is (fixes #3864)

* File › Options keeps the view where it is too

Changing a setting such as spell checking moves neither the caret nor
the text, so it no longer scrolls back to the caret.

* Layout: an empty paragraph with a hidden mark shows nothing, not even its list number (#3982) (#3992)

* Layout: an empty paragraph with a hidden mark shows nothing, not even its list number (#3982)

A paragraph whose mark is hidden and whose text is empty or hidden too is
now laid out as nothing, like a paragraph deleted in the final text: no
room, no list number and no shading. Its number is still counted, as in
Word ("1. Alpha", hidden item, "3. Beta"). Before, the body gave it no
room but drew its number under the next item's, and a table cell gave it
a line of its own with the number.

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouch…

* Tables: a row fits on a page only with room for the border below it (#3814) (#3815)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Tables: a row fits on a page only with room for the border below it

Word places a table row on a page only when the border under it fits too:
that border belongs to the next row's top band, but the row is moved on when
the page has its height and not the border. WordCraft checked the row's height
alone, so a page could hold one row more than Word's.

RowLayout::edge is the width of the bottom border of a row that another row
follows; place_table counts it when it checks whether a row fits, and so does
the check that keeps repeated header rows from staying alone at the foot of a
page. The part of a split row left on the page has none (it ends the page).

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Autho…

* #3734: ui.resize refuses sizes that aren't positive and clamps to the GPU's limit (#3920)

* #3734: ui.resize refuses sizes that aren't positive and clamps to the GPU's limit

The control channel forwarded any width/height to the viewport. Zero, negative and huge sizes now get an error or a size the window can have: the app's minimum and the largest texture the GPU draws (at most 16384 points). The reply carries the size asked for.

* #3734: refuse non-finite sizes, and let the GPU limit win over the minimum

Review follow-up: an infinite side (1e300 once read as f32) is refused rather than clamped, and a GPU limit below the app's minimum caps the size instead of being raised to it.

* Backstage: non-Latin file names in Recent and Recover draw, not boxes (#3545)

File › Home's Recent list and File › Recover draw file names with the
interface faces, which have no CJK, Arabic, Hebrew… glyphs; nothing asked
for a covering face the way the window caption and comment balloons do, so
a name like 小红书帖子-草稿.md showed as a row of boxes. A new
ensure_backstage_fonts does what ensure_caption_fonts does for those names:
CJK loads the CJK UI font, other scripts borrow the document fallback faces,
re-checked only when the names change.

Regression test fails without the call; checked live on the Recover page.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Print and PDF export use the Numeral setting (#3784) (#3824)

* Fix .doc bookmark table desync on names longer than 40 characters (#3949)

* Fix .doc bookmark table desync on names longer than 40 characters

Fixes #3690

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a re…

* Status bar names the page in view, not only the caret's page (#3880) (#3901)

* Inline shared Word 2003 headers, notes and comments once (#3985)

Fixes #3652

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* G14: Romanian: translate the 68 rows that carried English text; feminine stângă/dreaptă, unambiguous arrows and brackets (#3558)

The integration of #3203 filled the keys the catalog lacked with English text.
These are now translated (cm, mm and pi stay as they are). 24 labels take the
feminine stângă/dreaptă (Bordură stângă, Margine dreaptă), arrows read
"spre stânga/dreapta" and brackets say which bracket, so "paranteză dreaptă"
(square bracket) never meets dreapta (right).


Claude-Session: https://claude.ai/code/session_01JadgGo4G2GHaAMNb3qyygk

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Keep inline content and formatting in Convert Text to Table (#3937)

Fixes #3751

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Compare: don't let a character-level diff scramble a reordered digit run (#3224)

lcs_ops is a textbook LCS, which finds the mathematically longest common
subsequence, not the most readable edit script. On two short strings that
happen to share a few characters in a different order (very common with
dates, IDs, phone numbers), it faithfully threads a chain of single-character
matches through what a reader expects to be one clean replacement — e.g.
"01.09.2026" -> "19.10.2026" came out as an alternating run of tiny
deletions and insertions instead of deleting "01.09" and inserting "19.10".

Add collapse_noise_matches, called only for character-level compares: it
folds a short (length-1) match that sits strictly inside a run of edits back
into the surrounding delete/insert, flattening the region into one clean
delete followed by one clean insert. A match that opens or closes the whole
sequence is the genuine common prefix/suffix lcs_ops already stripped before
running its table, so it's never touched regardless of length, and the fold
is gated on character granularity only — a matched whole word at word-level
compare is essentially never a coincidence and must survive.

Fixes #3094.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* Regression tests for #2 and #504 (#3860)

* #2: test that style gallery labels are truncated and hanging previews stay in view

Long custom style names were painted at full width and ran into neighbouring tiles, and a style with a hanging indent larger than its left indent previewed with its first line off the tile's left edge. The tests render the Styles gallery with one such style: the label must be no wider than its 74-point tile and elided, and the preview's text must start at the tile's left edge and stay whole. Without the fix the label measures 291 points and the preview's ink spans only columns 0..24 (cut by the edge).

* #504: test that the Backstage side menu edge is not a resize handle

The File side menu is a fixed 200-point panel, but egui left it resizable, so hovering its right edge drew a resize line and showed a resize cursor that did nothing. The test hovers the edge and checks the pointer is not a resize cursor. Without the fix it reports ResizeEast.

* Restrict Editing: read only and comments-only protection grey out edits (#3264)

* Restrict Editing: read only and comments-only protection grey out edits

Without an exception the author may edit, a read-only or comments-only
document refuses every edit, but the ribbon drew the buttons live.
greyed_out now covers every refusal that doesn't depend on params or the
selection (read only, comments only, forms without fillable controls,
Accept/Reject under tracked-changes protection), and check_before refuses
through it. The ribbon reads the facts that decide it (exceptions for the
author, fillable controls) from a cache keyed by document, revision and
author; the engine's own check computes them fresh.

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

* Restrict Editing: protection refuses changing whether moves are tracked

Track Changes Options stays available under protection (its options are the
user's own), but trackMoves edits the document's setting, so it passes the
same check as an edit and isn't limited to an exception.

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that …

* Font: a selected text box's Font commands format all its text (#3898)

* Font: a selected text box's Font commands format all its text

With a text box selected as an object (its frame, not text in it), Word's
Font group formats all the box's text. WordCraft formatted only the box's
anchor character in the paragraph around it, so the box's text kept its
font. Select All doesn't reach a floating box's text either (as in Word),
which left no way to change a box's font short of editing inside it, and
none when the box sits behind text, where a click goes to the text over it.

format::apply and the toggles' reading of the current formatting now take
a selected text box's whole story, as the WordArt commands already do.

* Contributors: pakiat's display name

* Font: Change Case, Clear and Reset on a selected text box work on its text

With a text box selected as an object, Word's Change Case, Clear All
Formatting and Reset Character Formatting work on the box's text and leave
the body text around it alone (observed black-box). format.changeCase,
format.clear and format.resetChar used the box's anchor in the body
instead, so Clear could reset the surrounding paragraph while the box kept
its formatting.

They now take a selected text box's whole story, as the other Font
commands do since the previous commit. An empty selected box also reads
its paragraph mark for the toggles' current state (it read the anchor's
run around it), so Bold on an empty box in bold body text turns Bold on.

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

* Font: a selected text box shows its own paragraph style

With a text box selected as an object, format.state reported the box's
character formatting but the style of the body paragraph the box is
anchored in. Word shows the box's own style then (observed black-box: a
Heading 1 box in a Normal paragraph shows Heading 1). The style and its
name now come from the box's first paragraph.

Also doc comments for format.changeCase and format.clear.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* packaging: use [[ for bash conditional tests (SonarCloud S7688) (#4024)

SonarCloud flags every bare [ conditional in bash scripts under packaging/ as a reliability smell (rule S7688). Convert the flagged [ ... ] tests to [[ ... ]]: safer word-splitting and no glob surprises, same semantics for these string and file tests.

* A document with many relationships takes minutes to open (#3809)

* The reader takes the referenced relationship ids from the markup's attributes

Opening a document looked for each relationship's id in the main document's
part, to know which parts the markup already refers to so they aren't
carried over twice. That was a full pass over the part, once per
relationship: the open cost was the part's size times the number of
relationships, where parsing the same part costs its size once. With the
reader's limits — a 256 MiB part and 1024 relationships — that is about
256 GiB of byte comparisons for one document.

The ids the main document refers to are the ones it names in a relationship
attribute (`r:id`, VML's `o:relid`) — the same rule the embedded objects
use. The reader has already parsed the main document, so the ids are taken
from that tree: one pass over the markup it has, no search over the part at
all, and no second parse. The set of ids is bounded by the relationships the
reader looks at, so a file with a large relationship part does not make the
open allocate for ids no lookup can reach.

An id that only appears as text is not a reference. The text names no part,
and treating it as one lost the part on save: the relationship was skipped
as "already referred to", so its target was carried neither by the element
that names it nor as an unmodelled part.

On a 64 MiB main part, opening a document with 1024 relationships went from
224 seconds to 0.20 — and the file is 70 KB on disk, so it was a way to make
the app spend minutes of CPU on a small input.

#3411

* Record the relationship-scan fix in the progress docs

The file-format parity document says how the ids are collected and what the
open costs before and after; the ranked gap list holds one row per gap now,
and its #20 row carries the measured figure. The progress log and the
revision history gain the entry.

#3411

* #3411: read a relationship the markup names without the prefix

The reader resolves a relationship id through `El::attr`, which accepts an
unqualified attribute of the same local name (a producer that left the
prefix out writes `id="rId5"`). The relationship-id handling did not: it
counted only the attributes in the relationship namespace and VML's
`o:relid`, so an id written that way named no part.

One context-aware helper now decides what a relationship attribute is, for
the reader and the writer alike (`xml::is_rel_attr`): the relationship
namespace and `o:relid` always are, an unqualified attribute is one on an
element that carries a relationship and only with the local name of a
relationship attribute, and an ordinary `id` — a VML shape's, a drawing's
`wp:docPr`, a picture's `pic:cNvPr`, a page background's — is the
element's own and names no part.

With it, an OLE object's picture whose image is named by an unqualified
`id` keeps its image: the part is kept with the object, and the writer
keeps the attribute and rewrites it with the id the new package holds
the image under. Before, the image was dropped and the written object
pointed at a relationship that was not there.

Tests: `docx::read::embed::tests` (a hyperlink's unqualified `id` names
the part, a VML shape's does not), `docx::xml::tests` (the helper),
`docx::tests::fixtures` (the round trip: the attribute is preserved and
rewritten to the id the new package gives the part).

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live pr…

* A word with no break opportunity in it makes the layout quadratic (#3808)

* A word with no break opportunity in it no longer makes the layout quadratic

Laying out a line beside a floating object measures the word that starts on
it, to tell whether the word fits the full line. The measurement walked to
the end of the paragraph every time, so a word with no break opportunity in
it — which takes a line per few characters — walked the paragraph once per
line, and the layout was quadratic in the word's length.

The measurement now stops once the word is clearly wider than the room there
is: the answer is known by then, and the advances are widths, so the rest of
the word could only make it wider — unless a font pulls a glyph back with a
negative advance, and then the measurement carries on only as far as the rest
of the word could take the width back.

The same answer, the same layout, one line's worth of clusters per line
instead of a paragraph's. A 1 MiB word went from 29.42 s to 0.35 s, and a
4 MiB one from 472.34 s to 1.38 s.

#3412

* Record the layout fix in the progress docs

The layout parity table gains the measurement for a word with no break
opportunity in it laid out beside a floating object, and a note says what
the fix was. The ranked gap list holds one row per gap now, and its #20 row
carries the measured figure. The progress log and the revision history gain
the entry.

#3412

* Measure every word once, before the line is broken (#3412)

The measurement of the word that starts a line walked the word to find out how
wide it was, and a font that pulls a glyph back with a negative advance keeps
the walk going: the bound that stopped it early was the sum of the advances the
rest of the word could give back, and when the advances alternate in sign that
sum stays so negative that the walk never stops early.

Every word is measured once now, before the line is broken: the width of a word
is the sum of its advances, the negative ones included, and asking whether the
word fits the full line is a lookup rather than another pass over it.

The answer is the same: the sum is taken over the same clusters the scan took
them over. It is added up from the word's end rather than its start, which is
the same number in all but the last bits of a float — the sum a float holds
depends on the order it is added in, and the two differ by far less than the
tenth of a point the comparison that uses the width allows.

The ranked-gap rows that were not this fix's are back to how main has them.

* Lay the negative-advance word out beside a float (#3412)

The width of a word is checked against the width of a word, and a font that
pulls a glyph back takes the width off the rest of the word as well. The
layout now does that too: the word is laid out beside a floating object, in
the strip the object leaves, and it fits there — where the same word without
the glyph pulled back does not, and moves on whole below the object instead.

The strip the object leaves is half way between the two widths, so the test
does not depend on the font's metrics.

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the ne…

* doc: preserve rotated free endpoints when rerouting connectors (#3882)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* doc: preserve rotated free endpoints when rerouting connectors

Transform fallback connector endpoints through the existing rotation and flips around the frame centre before rerouting clears rotation. Attached endpoints continue resolving from target sites; free or unresolved endpoints retain their visible positions.

Keep current shape outline behavior and cover connector kinds, either endpoint, rotations and flips, unresolved targets, invalid sites and settled rerouting. Document the model-level correction without changing parity estimates.

Model: openai-codex/gpt-6.1-sol

* fix(connectors): preserve displayed endpoints in scaled groups

Pass the whole group into reroute_group so fallback endpoints use the same scaled and capped child rectangle as rendering. Apply child spin after group scaling, then inverse-map displayed endpoints to group coordinates before routing. Keep resolved connection-site routing unchanged and migrate the engine caller.

Avoid rewriting fallback connectors when degenerate scales or the display-size cap prevent representing their displayed geometry. Add regressions for nonuniform scaling, rotations, flips, unresolved sites, and capped reroutes, and update the existing connector documentation.

Model: openai-codex/gpt-6.1-sol

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <norepl…

* Selection bar: right margin for right-to-left text, and beside table rows (UI gap) (#2042)

* gap: selection bar RTL/table rows WIP

* gap: selection bar in the right margin for right-to-left paragraphs and beside table rows

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

* gap: selection bar review fixes (row bar only beside the table, Shift+click from outside, text-box tables)

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

* gap: selection bar review round 2 (0.5pt row-overlap tolerance, body-story check for Shift+click)

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

* gap: selection bar: triple-click or Ctrl+click beside a table row selects the document, as in Word

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

* gap: selection bar: ui-parity wording (only triple-click was checked in Word)

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

* gap: selection bar: fix docs after rebase onto main

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

* gap: selection bar: a Shift+click drag keeps the anchor row of its press

table.selectAt takes an optional anchorRow; the app reads the anchor at the
press and sends it on every drag frame, so pointer jitter after an upward
Shift+click no longer re-reads it from the rows the press selected.

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

* Selection bar RTL test: the left margin shows no bar arrow (Windows reports no cursor there)

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

* #2042: repair three doc status lines a #3882 merge left with a stray Target

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>

* #3894: queue the Print request through Session::push_ui_request (bounded since #3997)

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>
Co-authored-by: dfgodvan <dfgodvan@gmail.com>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Michael Higgins <55243596+Higgs32584@users.noreply.github.com>
Co-authored-by: In Mean <6434741+inmean@users.noreply.github.com>
Co-authored-by: Jayesh Prajapati <156836276+Jayesh-Prajapati9@users.noreply.github.com>
Co-authored-by: Gabriel Matozo <69991354+GabrielMatozo@users.noreply.github.com>
Co-authored-by: Mathieu Rancourt <40959576+MatRanc@users.noreply.github.com>
Co-authored-by: pakiat <49967840+pakiat@users.noreply.github.com>
Co-authored-by: Marco Arnulfo <103679419+marcoarnulfo@users.noreply.github.com>
Co-authored-by: Menonro <nitayd2001@gmail.com>
Co-authored-by: Gabriel Luís da Silva <79342117+mercuryVM@users.noreply.github.com>
Co-authored-by: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com>
Co-authored-by: dhruvLearner8 <83899022+dhruvLearner8@users.noreply.github.com>
Co-authored-by: vioreltus <142961713+vioreltus@users.noreply.github.com>
Co-authored-by: Dominik Strebel <dominik.strebel@gmail.com>
Co-authored-by: Mat Strange <54028700+matstrange@users.noreply.github.com>
Co-authored-by: Dmitriy <dmitriy@mrzv.org>
Co-authored-by: Dmitrii Khlapov <dkhlapov@pm.me>
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
2026-10-23 13:30:42 -04:00
38931c34be Selection bar: right margin for right-to-left text, and beside table rows (UI gap) (#2042)
* gap: selection bar RTL/table rows WIP

* gap: selection bar in the right margin for right-to-left paragraphs and beside table rows

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

* gap: selection bar review fixes (row bar only beside the table, Shift+click from outside, text-box tables)

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

* gap: selection bar review round 2 (0.5pt row-overlap tolerance, body-story check for Shift+click)

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

* gap: selection bar: triple-click or Ctrl+click beside a table row selects the document, as in Word

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

* gap: selection bar: ui-parity wording (only triple-click was checked in Word)

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

* gap: selection bar: fix docs after rebase onto main

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

* gap: selection bar: a Shift+click drag keeps the anchor row of its press

table.selectAt takes an optional anchorRow; the app reads the anchor at the
press and sends it on every drag frame, so pointer jitter after an upward
Shift+click no longer re-reads it from the rows the press selected.

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

* Selection bar RTL test: the left margin shows no bar arrow (Windows reports no cursor there)

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

* #2042: repair three doc status lines a #3882 merge left with a stray Target

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-23 13:24:33 -04:00
+24 fa5992223f doc: preserve rotated free endpoints when rerouting connectors (#3882)
* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* doc: preserve rotated free endpoints when rerouting connectors

Transform fallback connector endpoints through the existing rotation and flips around the frame centre before rerouting clears rotation. Attached endpoints continue resolving from target sites; free or unresolved endpoints retain their visible positions.

Keep current shape outline behavior and cover connector kinds, either endpoint, rotations and flips, unresolved targets, invalid sites and settled rerouting. Document the model-level correction without changing parity estimates.

Model: openai-codex/gpt-6.1-sol

* fix(connectors): preserve displayed endpoints in scaled groups

Pass the whole group into reroute_group so fallback endpoints use the same scaled and capped child rectangle as rendering. Apply child spin after group scaling, then inverse-map displayed endpoints to group coordinates before routing. Keep resolved connection-site routing unchanged and migrate the engine caller.

Avoid rewriting fallback connectors when degenerate scales or the display-size cap prevent representing their displayed geometry. Add regressions for nonuniform scaling, rotations, flips, unresolved sites, and capped reroutes, and update the existing connector documentation.

Model: openai-codex/gpt-6.1-sol

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* #3944: drop the wall-clock assert from the long-root-tag test

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

---------

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>
Co-authored-by: dfgodvan <dfgodvan@gmail.com>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>

* Write a text-box story shared by many shapes only once on docx save (#3983)

Fixes #3653

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Keep pending formatting revisions when Style Inspector clears formatting (#3971)

Fixes #3671

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the headless ui_requests queue and keep one Paste Special request (#3997)

Fixes #3641

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Validate PNG extension in file.exportPng (#3837)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* changes to filter dialogue

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-au…

* Khmer: ignore w14:ligatures when shaping Khmer text (#3921)

Documents that Word saves with w14:ligatures="historicalDiscretional" (every Khmer run
in typical Khmer OS files) turned liga and clig off. Khmer OS fonts build their
syllables there, so pre-base vowels and stacked signs came out in the wrong order
(ស៊ើបអង្កេត drawn as សីបអង្កតេ). Khmer text is now shaped with the font's defaults,
as Word does.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Skip quoted attribute values when scanning settings.xml tags (#3954)

Fixes #3682

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Rename kept docx parts whose names are not valid OPC part names (#3947)

* Rename kept docx parts whose names are not valid OPC part names

Fixes #3691

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its re…

* #3637: A table cell span from a .json file no longer overflows (#3927)

Insert Column, Merge Cells and Delete Column did arithmetic on the raw
CellProps::span, which a .json document can set to any u32, so a span
near u32::MAX overflowed (a panic in debug, a wrapped span in release).
They now work from the clamped span() and cap the result at MAX_COLS.

* #3663: "NaN" in the Font dialog's Size box no longer closes the app (#3928)

* #3663: "NaN" in the Font dialog's Size box no longer closes the app

"NaN" parses as an f32 and clamp keeps NaN, so the preview font size was
NaN and epaint's glyph assertion panicked while painting the dialog. The
preview now drops non-finite sizes and shows 12 pt.

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrel…

* Load ui.json with many unknown keys in linear time (#3968)

* Load ui.json with many unknown keys in linear time

Fixes #3659

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so…

* docbin: mark the format-mandated SHA-1 as reviewed (SonarCloud S4790) (#4020)

MS-OFFCRYPTO §2.3.5 derives the RC4 CryptoAPI key with SHA-1, so the
digest cannot be strengthened without breaking Word-written .doc files.
The NOSONAR marker records that this use is required by the format.

* Cap and deduplicate the .aff TRY alphabet at parse time (#3981)

Fixes #3654

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Render: a dimmed header's borders keep one tone where they meet (#2946) (#3725)

#2947 fixed the fade by drawing a dimmed layer inside a single opacity layer,
but left no regression test for the tone: where a header table's borders cross,
a crossed or shared rule would quietly come out darker than a rule that is not
drawn on top of anything, and the undimmed view and the PDF export would not
agree with the screen.

The test renders the same page with the header dimmed and undimmed and checks
three rules: the darkest pixel at a crossing of a cell's top and left rules,
the darkest pixel on a border two neighbouring cells share, and one on an
outer border well clear of both. With the fade done as one layer, the
crossing and the shared border keep the tone of an isolated rule; measured
against a clean full-strength render, the dimmed tone is 0.45 of the
background, not 0.45 squared.

* Refuse arrange.group when members exceed the group child cap (#3958)

Fixes #3679

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Ignore docx chart number-format codes longer than 255 bytes (#3963)

Fixes #3729

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Format Painter: show the copy pointer over the text while armed (#3453) (#3560)

* Format Painter: show the copy pointer over the text while armed (#3453)

The canvas chose the I-beam over text whether or not the Format Painter
held copied formatting, so nothing showed the tool was waiting for a
click. While it is armed, the pointer over the text is now the copy
pointer (egui has no paintbrush), the same choice as the open DeckCraft
fix for the same report (storytold/deckcraft#846). Pictures, table
handles and resize edges keep their own pointers.

* Format Painter: the selection bar keeps its arrow while the painter is armed

Review follow-up: the bar's arrow shows that a click there selects whole
lines, so it outranks the painter's copy pointer (#3453).

* Format Painter pointer test: the selection bar keeps its arrow while armed

Review follow-up (#3453): the test now also hovers the selection bar with
the painter armed, so a precedence regression there fails it.

* Canvas: document show()

Review follow-up (#3453): CodeRabbit's docstring check counts the
function the pointer change sits in.

* #3453: Format Painter pointer test requires the I-beam once the painter is off

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

* #3453: Format Painter pointer test expects the hidden pointer where the I-beam is drawn

On Windows the canvas hides the system I-beam and draws its own (#1746), so
the pointer over the text reads None there, not Text.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Ruler: dragging a margin edge leaves the view where it is (#3900)

Each step of a margin drag on a ruler runs layout.margins, and like any
command that brought the caret back into view. With the caret off screen
(dragging the bottom margin while the text being edited is near the top
of the page) the view scrolled mid-drag: the page slid under the pointer,
the pointer's distance from the page's top changed with it, and the margin
ran far past where it was dropped. A drag of about 1 cm on the vertical
ruler set a 2 cm bottom margin to 8.1 cm.

The margin handles now run the command with run_keep_view, as table border
drags do (#1738).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Save, export and print keep the view where it is (#3895)

* Save, export and print keep the view where it is (fixes #3864)

* File › Options keeps the view where it is too

Changing a setting such as spell checking moves neither the caret nor
the text, so it no longer scrolls back to the caret.

* Layout: an empty paragraph with a hidden mark shows nothing, not even its list number (#3982) (#3992)

* Layout: an empty paragraph with a hidden mark shows nothing, not even its list number (#3982)

A paragraph whose mark is hidden and whose text is empty or hidden too is
now laid out as nothing, like a paragraph deleted in the final text: no
room, no list number and no shading. Its number is still counted, as in
Word ("1. Alpha", hidden item, "3. Beta"). Before, the body gave it no
room but drew its number under the next item's, and a table cell gave it
a line of its own with the number.

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouch…

* Tables: a row fits on a page only with room for the border below it (#3814) (#3815)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Tables: a row fits on a page only with room for the border below it

Word places a table row on a page only when the border under it fits too:
that border belongs to the next row's top band, but the row is moved on when
the page has its height and not the border. WordCraft checked the row's height
alone, so a page could hold one row more than Word's.

RowLayout::edge is the width of the bottom border of a row that another row
follows; place_table counts it when it checks whether a row fits, and so does
the check that keeps repeated header rows from staying alone at the foot of a
page. The part of a split row left on the page has none (it ends the page).

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Autho…

* #3734: ui.resize refuses sizes that aren't positive and clamps to the GPU's limit (#3920)

* #3734: ui.resize refuses sizes that aren't positive and clamps to the GPU's limit

The control channel forwarded any width/height to the viewport. Zero, negative and huge sizes now get an error or a size the window can have: the app's minimum and the largest texture the GPU draws (at most 16384 points). The reply carries the size asked for.

* #3734: refuse non-finite sizes, and let the GPU limit win over the minimum

Review follow-up: an infinite side (1e300 once read as f32) is refused rather than clamped, and a GPU limit below the app's minimum caps the size instead of being raised to it.

* Backstage: non-Latin file names in Recent and Recover draw, not boxes (#3545)

File › Home's Recent list and File › Recover draw file names with the
interface faces, which have no CJK, Arabic, Hebrew… glyphs; nothing asked
for a covering face the way the window caption and comment balloons do, so
a name like 小红书帖子-草稿.md showed as a row of boxes. A new
ensure_backstage_fonts does what ensure_caption_fonts does for those names:
CJK loads the CJK UI font, other scripts borrow the document fallback faces,
re-checked only when the names change.

Regression test fails without the call; checked live on the Recover page.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Print and PDF export use the Numeral setting (#3784) (#3824)

* Fix .doc bookmark table desync on names longer than 40 characters (#3949)

* Fix .doc bookmark table desync on names longer than 40 characters

Fixes #3690

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a re…

* Status bar names the page in view, not only the caret's page (#3880) (#3901)

* Inline shared Word 2003 headers, notes and comments once (#3985)

Fixes #3652

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* G14: Romanian: translate the 68 rows that carried English text; feminine stângă/dreaptă, unambiguous arrows and brackets (#3558)

The integration of #3203 filled the keys the catalog lacked with English text.
These are now translated (cm, mm and pi stay as they are). 24 labels take the
feminine stângă/dreaptă (Bordură stângă, Margine dreaptă), arrows read
"spre stânga/dreapta" and brackets say which bracket, so "paranteză dreaptă"
(square bracket) never meets dreapta (right).


Claude-Session: https://claude.ai/code/session_01JadgGo4G2GHaAMNb3qyygk

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Keep inline content and formatting in Convert Text to Table (#3937)

Fixes #3751

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Compare: don't let a character-level diff scramble a reordered digit run (#3224)

lcs_ops is a textbook LCS, which finds the mathematically longest common
subsequence, not the most readable edit script. On two short strings that
happen to share a few characters in a different order (very common with
dates, IDs, phone numbers), it faithfully threads a chain of single-character
matches through what a reader expects to be one clean replacement — e.g.
"01.09.2026" -> "19.10.2026" came out as an alternating run of tiny
deletions and insertions instead of deleting "01.09" and inserting "19.10".

Add collapse_noise_matches, called only for character-level compares: it
folds a short (length-1) match that sits strictly inside a run of edits back
into the surrounding delete/insert, flattening the region into one clean
delete followed by one clean insert. A match that opens or closes the whole
sequence is the genuine common prefix/suffix lcs_ops already stripped before
running its table, so it's never touched regardless of length, and the fold
is gated on character granularity only — a matched whole word at word-level
compare is essentially never a coincidence and must survive.

Fixes #3094.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* Regression tests for #2 and #504 (#3860)

* #2: test that style gallery labels are truncated and hanging previews stay in view

Long custom style names were painted at full width and ran into neighbouring tiles, and a style with a hanging indent larger than its left indent previewed with its first line off the tile's left edge. The tests render the Styles gallery with one such style: the label must be no wider than its 74-point tile and elided, and the preview's text must start at the tile's left edge and stay whole. Without the fix the label measures 291 points and the preview's ink spans only columns 0..24 (cut by the edge).

* #504: test that the Backstage side menu edge is not a resize handle

The File side menu is a fixed 200-point panel, but egui left it resizable, so hovering its right edge drew a resize line and showed a resize cursor that did nothing. The test hovers the edge and checks the pointer is not a resize cursor. Without the fix it reports ResizeEast.

* Restrict Editing: read only and comments-only protection grey out edits (#3264)

* Restrict Editing: read only and comments-only protection grey out edits

Without an exception the author may edit, a read-only or comments-only
document refuses every edit, but the ribbon drew the buttons live.
greyed_out now covers every refusal that doesn't depend on params or the
selection (read only, comments only, forms without fillable controls,
Accept/Reject under tracked-changes protection), and check_before refuses
through it. The ribbon reads the facts that decide it (exceptions for the
author, fillable controls) from a cache keyed by document, revision and
author; the engine's own check computes them fresh.

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

* Restrict Editing: protection refuses changing whether moves are tracked

Track Changes Options stays available under protection (its options are the
user's own), but trackMoves edits the document's setting, so it passes the
same check as an edit and isn't limited to an exception.

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that …

* Font: a selected text box's Font commands format all its text (#3898)

* Font: a selected text box's Font commands format all its text

With a text box selected as an object (its frame, not text in it), Word's
Font group formats all the box's text. WordCraft formatted only the box's
anchor character in the paragraph around it, so the box's text kept its
font. Select All doesn't reach a floating box's text either (as in Word),
which left no way to change a box's font short of editing inside it, and
none when the box sits behind text, where a click goes to the text over it.

format::apply and the toggles' reading of the current formatting now take
a selected text box's whole story, as the WordArt commands already do.

* Contributors: pakiat's display name

* Font: Change Case, Clear and Reset on a selected text box work on its text

With a text box selected as an object, Word's Change Case, Clear All
Formatting and Reset Character Formatting work on the box's text and leave
the body text around it alone (observed black-box). format.changeCase,
format.clear and format.resetChar used the box's anchor in the body
instead, so Clear could reset the surrounding paragraph while the box kept
its formatting.

They now take a selected text box's whole story, as the other Font
commands do since the previous commit. An empty selected box also reads
its paragraph mark for the toggles' current state (it read the anchor's
run around it), so Bold on an empty box in bold body text turns Bold on.

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

* Font: a selected text box shows its own paragraph style

With a text box selected as an object, format.state reported the box's
character formatting but the style of the body paragraph the box is
anchored in. Word shows the box's own style then (observed black-box: a
Heading 1 box in a Normal paragraph shows Heading 1). The style and its
name now come from the box's first paragraph.

Also doc comments for format.changeCase and format.clear.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* packaging: use [[ for bash conditional tests (SonarCloud S7688) (#4024)

SonarCloud flags every bare [ conditional in bash scripts under packaging/ as a reliability smell (rule S7688). Convert the flagged [ ... ] tests to [[ ... ]]: safer word-splitting and no glob surprises, same semantics for these string and file tests.

* A document with many relationships takes minutes to open (#3809)

* The reader takes the referenced relationship ids from the markup's attributes

Opening a document looked for each relationship's id in the main document's
part, to know which parts the markup already refers to so they aren't
carried over twice. That was a full pass over the part, once per
relationship: the open cost was the part's size times the number of
relationships, where parsing the same part costs its size once. With the
reader's limits — a 256 MiB part and 1024 relationships — that is about
256 GiB of byte comparisons for one document.

The ids the main document refers to are the ones it names in a relationship
attribute (`r:id`, VML's `o:relid`) — the same rule the embedded objects
use. The reader has already parsed the main document, so the ids are taken
from that tree: one pass over the markup it has, no search over the part at
all, and no second parse. The set of ids is bounded by the relationships the
reader looks at, so a file with a large relationship part does not make the
open allocate for ids no lookup can reach.

An id that only appears as text is not a reference. The text names no part,
and treating it as one lost the part on save: the relationship was skipped
as "already referred to", so its target was carried neither by the element
that names it nor as an unmodelled part.

On a 64 MiB main part, opening a document with 1024 relationships went from
224 seconds to 0.20 — and the file is 70 KB on disk, so it was a way to make
the app spend minutes of CPU on a small input.

#3411

* Record the relationship-scan fix in the progress docs

The file-format parity document says how the ids are collected and what the
open costs before and after; the ranked gap list holds one row per gap now,
and its #20 row carries the measured figure. The progress log and the
revision history gain the entry.

#3411

* #3411: read a relationship the markup names without the prefix

The reader resolves a relationship id through `El::attr`, which accepts an
unqualified attribute of the same local name (a producer that left the
prefix out writes `id="rId5"`). The relationship-id handling did not: it
counted only the attributes in the relationship namespace and VML's
`o:relid`, so an id written that way named no part.

One context-aware helper now decides what a relationship attribute is, for
the reader and the writer alike (`xml::is_rel_attr`): the relationship
namespace and `o:relid` always are, an unqualified attribute is one on an
element that carries a relationship and only with the local name of a
relationship attribute, and an ordinary `id` — a VML shape's, a drawing's
`wp:docPr`, a picture's `pic:cNvPr`, a page background's — is the
element's own and names no part.

With it, an OLE object's picture whose image is named by an unqualified
`id` keeps its image: the part is kept with the object, and the writer
keeps the attribute and rewrites it with the id the new package holds
the image under. Before, the image was dropped and the written object
pointed at a relationship that was not there.

Tests: `docx::read::embed::tests` (a hyperlink's unqualified `id` names
the part, a VML shape's does not), `docx::xml::tests` (the helper),
`docx::tests::fixtures` (the round trip: the attribute is preserved and
rewritten to the id the new package gives the part).

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

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

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

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

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live pr…

* A word with no break opportunity in it makes the layout quadratic (#3808)

* A word with no break opportunity in it no longer makes the layout quadratic

Laying out a line beside a floating object measures the word that starts on
it, to tell whether the word fits the full line. The measurement walked to
the end of the paragraph every time, so a word with no break opportunity in
it — which takes a line per few characters — walked the paragraph once per
line, and the layout was quadratic in the word's length.

The measurement now stops once the word is clearly wider than the room there
is: the answer is known by then, and the advances are widths, so the rest of
the word could only make it wider — unless a font pulls a glyph back with a
negative advance, and then the measurement carries on only as far as the rest
of the word could take the width back.

The same answer, the same layout, one line's worth of clusters per line
instead of a paragraph's. A 1 MiB word went from 29.42 s to 0.35 s, and a
4 MiB one from 472.34 s to 1.38 s.

#3412

* Record the layout fix in the progress docs

The layout parity table gains the measurement for a word with no break
opportunity in it laid out beside a floating object, and a note says what
the fix was. The ranked gap list holds one row per gap now, and its #20 row
carries the measured figure. The progress log and the revision history gain
the entry.

#3412

* Measure every word once, before the line is broken (#3412)

The measurement of the word that starts a line walked the word to find out how
wide it was, and a font that pulls a glyph back with a negative advance keeps
the walk going: the bound that stopped it early was the sum of the advances the
rest of the word could give back, and when the advances alternate in sign that
sum stays so negative that the walk never stops early.

Every word is measured once now, before the line is broken: the width of a word
is the sum of its advances, the negative ones included, and asking whether the
word fits the full line is a lookup rather than another pass over it.

The answer is the same: the sum is taken over the same clusters the scan took
them over. It is added up from the word's end rather than its start, which is
the same number in all but the last bits of a float — the sum a float holds
depends on the order it is added in, and the two differ by far less than the
tenth of a point the comparison that uses the width allows.

The ranked-gap rows that were not this fix's are back to how main has them.

* Lay the negative-advance word out beside a float (#3412)

The width of a word is checked against the width of a word, and a font that
pulls a glyph back takes the width off the rest of the word as well. The
layout now does that too: the word is laid out beside a floating object, in
the strip the object leaves, and it fits there — where the same word without
the glyph pulled back does not, and moves on whole below the object instead.

The strip the object leaves is half way between the two widths, so the test
does not depend on the font's metrics.

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the ne…

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>
Co-authored-by: dfgodvan <dfgodvan@gmail.com>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Michael Higgins <55243596+Higgs32584@users.noreply.github.com>
Co-authored-by: In Mean <6434741+inmean@users.noreply.github.com>
Co-authored-by: Jayesh Prajapati <156836276+Jayesh-Prajapati9@users.noreply.github.com>
Co-authored-by: Gabriel Matozo <69991354+GabrielMatozo@users.noreply.github.com>
Co-authored-by: Mathieu Rancourt <40959576+MatRanc@users.noreply.github.com>
Co-authored-by: pakiat <49967840+pakiat@users.noreply.github.com>
Co-authored-by: Marco Arnulfo <103679419+marcoarnulfo@users.noreply.github.com>
Co-authored-by: Menonro <nitayd2001@gmail.com>
Co-authored-by: Gabriel Luís da Silva <79342117+mercuryVM@users.noreply.github.com>
Co-authored-by: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com>
Co-authored-by: dhruvLearner8 <83899022+dhruvLearner8@users.noreply.github.com>
Co-authored-by: vioreltus <142961713+vioreltus@users.noreply.github.com>
Co-authored-by: Dominik Strebel <dominik.strebel@gmail.com>
Co-authored-by: Mat Strange <54028700+matstrange@users.noreply.github.com>
2026-10-23 13:14:15 -04:00
pakiatandClaude Opus 5.5 9fb78ba8f1 Font: a selected text box's Font commands format all its text (#3898)
* Font: a selected text box's Font commands format all its text

With a text box selected as an object (its frame, not text in it), Word's
Font group formats all the box's text. WordCraft formatted only the box's
anchor character in the paragraph around it, so the box's text kept its
font. Select All doesn't reach a floating box's text either (as in Word),
which left no way to change a box's font short of editing inside it, and
none when the box sits behind text, where a click goes to the text over it.

format::apply and the toggles' reading of the current formatting now take
a selected text box's whole story, as the WordArt commands already do.

* Contributors: pakiat's display name

* Font: Change Case, Clear and Reset on a selected text box work on its text

With a text box selected as an object, Word's Change Case, Clear All
Formatting and Reset Character Formatting work on the box's text and leave
the body text around it alone (observed black-box). format.changeCase,
format.clear and format.resetChar used the box's anchor in the body
instead, so Clear could reset the surrounding paragraph while the box kept
its formatting.

They now take a selected text box's whole story, as the other Font
commands do since the previous commit. An empty selected box also reads
its paragraph mark for the toggles' current state (it read the anchor's
run around it), so Bold on an empty box in bold body text turns Bold on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Font: a selected text box shows its own paragraph style

With a text box selected as an object, format.state reported the box's
character formatting but the style of the body paragraph the box is
anchored in. Word shows the box's own style then (observed black-box: a
Heading 1 box in a Normal paragraph shows Heading 1). The style and its
name now come from the box's first paragraph.

Also doc comments for format.changeCase and format.clear.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:51:18 -04:00
+22 6fcfea4476 Restrict Editing: read only and comments-only protection grey out edits (#3264)
* Restrict Editing: read only and comments-only protection grey out edits

Without an exception the author may edit, a read-only or comments-only
document refuses every edit, but the ribbon drew the buttons live.
greyed_out now covers every refusal that doesn't depend on params or the
selection (read only, comments only, forms without fillable controls,
Accept/Reject under tracked-changes protection), and check_before refuses
through it. The ribbon reads the facts that decide it (exceptions for the
author, fillable controls) from a cache keyed by document, revision and
author; the engine's own check computes them fresh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Restrict Editing: protection refuses changing whether moves are tracked

Track Changes Options stays available under protection (its options are the
user's own), but trackMoves edits the document's setting, so it passes the
same check as an edit and isn't limited to an exception.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* #3944: drop the wall-clock assert from the long-root-tag test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>
Co-authored-by: dfgodvan <dfgodvan@gmail.com>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>

* Write a text-box story shared by many shapes only once on docx save (#3983)

Fixes #3653

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Keep pending formatting revisions when Style Inspector clears formatting (#3971)

Fixes #3671

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the headless ui_requests queue and keep one Paste Special request (#3997)

Fixes #3641

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Validate PNG extension in file.exportPng (#3837)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* changes to filter dialogue

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-au…

* Khmer: ignore w14:ligatures when shaping Khmer text (#3921)

Documents that Word saves with w14:ligatures="historicalDiscretional" (every Khmer run
in typical Khmer OS files) turned liga and clig off. Khmer OS fonts build their
syllables there, so pre-base vowels and stacked signs came out in the wrong order
(ស៊ើបអង្កេត drawn as សីបអង្កតេ). Khmer text is now shaped with the font's defaults,
as Word does.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Skip quoted attribute values when scanning settings.xml tags (#3954)

Fixes #3682

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Rename kept docx parts whose names are not valid OPC part names (#3947)

* Rename kept docx parts whose names are not valid OPC part names

Fixes #3691

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its re…

* #3637: A table cell span from a .json file no longer overflows (#3927)

Insert Column, Merge Cells and Delete Column did arithmetic on the raw
CellProps::span, which a .json document can set to any u32, so a span
near u32::MAX overflowed (a panic in debug, a wrapped span in release).
They now work from the clamped span() and cap the result at MAX_COLS.

* #3663: "NaN" in the Font dialog's Size box no longer closes the app (#3928)

* #3663: "NaN" in the Font dialog's Size box no longer closes the app

"NaN" parses as an f32 and clamp keeps NaN, so the preview font size was
NaN and epaint's glyph assertion panicked while painting the dialog. The
preview now drops non-finite sizes and shows 12 pt.

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrel…

* Load ui.json with many unknown keys in linear time (#3968)

* Load ui.json with many unknown keys in linear time

Fixes #3659

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so…

* docbin: mark the format-mandated SHA-1 as reviewed (SonarCloud S4790) (#4020)

MS-OFFCRYPTO §2.3.5 derives the RC4 CryptoAPI key with SHA-1, so the
digest cannot be strengthened without breaking Word-written .doc files.
The NOSONAR marker records that this use is required by the format.

* Cap and deduplicate the .aff TRY alphabet at parse time (#3981)

Fixes #3654

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Render: a dimmed header's borders keep one tone where they meet (#2946) (#3725)

#2947 fixed the fade by drawing a dimmed layer inside a single opacity layer,
but left no regression test for the tone: where a header table's borders cross,
a crossed or shared rule would quietly come out darker than a rule that is not
drawn on top of anything, and the undimmed view and the PDF export would not
agree with the screen.

The test renders the same page with the header dimmed and undimmed and checks
three rules: the darkest pixel at a crossing of a cell's top and left rules,
the darkest pixel on a border two neighbouring cells share, and one on an
outer border well clear of both. With the fade done as one layer, the
crossing and the shared border keep the tone of an isolated rule; measured
against a clean full-strength render, the dimmed tone is 0.45 of the
background, not 0.45 squared.

* Refuse arrange.group when members exceed the group child cap (#3958)

Fixes #3679

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Ignore docx chart number-format codes longer than 255 bytes (#3963)

Fixes #3729

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Format Painter: show the copy pointer over the text while armed (#3453) (#3560)

* Format Painter: show the copy pointer over the text while armed (#3453)

The canvas chose the I-beam over text whether or not the Format Painter
held copied formatting, so nothing showed the tool was waiting for a
click. While it is armed, the pointer over the text is now the copy
pointer (egui has no paintbrush), the same choice as the open DeckCraft
fix for the same report (storytold/deckcraft#846). Pictures, table
handles and resize edges keep their own pointers.

* Format Painter: the selection bar keeps its arrow while the painter is armed

Review follow-up: the bar's arrow shows that a click there selects whole
lines, so it outranks the painter's copy pointer (#3453).

* Format Painter pointer test: the selection bar keeps its arrow while armed

Review follow-up (#3453): the test now also hovers the selection bar with
the painter armed, so a precedence regression there fails it.

* Canvas: document show()

Review follow-up (#3453): CodeRabbit's docstring check counts the
function the pointer change sits in.

* #3453: Format Painter pointer test requires the I-beam once the painter is off

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #3453: Format Painter pointer test expects the hidden pointer where the I-beam is drawn

On Windows the canvas hides the system I-beam and draws its own (#1746), so
the pointer over the text reads None there, not Text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Ruler: dragging a margin edge leaves the view where it is (#3900)

Each step of a margin drag on a ruler runs layout.margins, and like any
command that brought the caret back into view. With the caret off screen
(dragging the bottom margin while the text being edited is near the top
of the page) the view scrolled mid-drag: the page slid under the pointer,
the pointer's distance from the page's top changed with it, and the margin
ran far past where it was dropped. A drag of about 1 cm on the vertical
ruler set a 2 cm bottom margin to 8.1 cm.

The margin handles now run the command with run_keep_view, as table border
drags do (#1738).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Save, export and print keep the view where it is (#3895)

* Save, export and print keep the view where it is (fixes #3864)

* File › Options keeps the view where it is too

Changing a setting such as spell checking moves neither the caret nor
the text, so it no longer scrolls back to the caret.

* Layout: an empty paragraph with a hidden mark shows nothing, not even its list number (#3982) (#3992)

* Layout: an empty paragraph with a hidden mark shows nothing, not even its list number (#3982)

A paragraph whose mark is hidden and whose text is empty or hidden too is
now laid out as nothing, like a paragraph deleted in the final text: no
room, no list number and no shading. Its number is still counted, as in
Word ("1. Alpha", hidden item, "3. Beta"). Before, the body gave it no
room but drew its number under the next item's, and a table cell gave it
a line of its own with the number.

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouch…

* Tables: a row fits on a page only with room for the border below it (#3814) (#3815)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Tables: a row fits on a page only with room for the border below it

Word places a table row on a page only when the border under it fits too:
that border belongs to the next row's top band, but the row is moved on when
the page has its height and not the border. WordCraft checked the row's height
alone, so a page could hold one row more than Word's.

RowLayout::edge is the width of the bottom border of a row that another row
follows; place_table counts it when it checks whether a row fits, and so does
the check that keeps repeated header rows from staying alone at the foot of a
page. The part of a split row left on the page has none (it ends the page).

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Autho…

* #3734: ui.resize refuses sizes that aren't positive and clamps to the GPU's limit (#3920)

* #3734: ui.resize refuses sizes that aren't positive and clamps to the GPU's limit

The control channel forwarded any width/height to the viewport. Zero, negative and huge sizes now get an error or a size the window can have: the app's minimum and the largest texture the GPU draws (at most 16384 points). The reply carries the size asked for.

* #3734: refuse non-finite sizes, and let the GPU limit win over the minimum

Review follow-up: an infinite side (1e300 once read as f32) is refused rather than clamped, and a GPU limit below the app's minimum caps the size instead of being raised to it.

* Backstage: non-Latin file names in Recent and Recover draw, not boxes (#3545)

File › Home's Recent list and File › Recover draw file names with the
interface faces, which have no CJK, Arabic, Hebrew… glyphs; nothing asked
for a covering face the way the window caption and comment balloons do, so
a name like 小红书帖子-草稿.md showed as a row of boxes. A new
ensure_backstage_fonts does what ensure_caption_fonts does for those names:
CJK loads the CJK UI font, other scripts borrow the document fallback faces,
re-checked only when the names change.

Regression test fails without the call; checked live on the Recover page.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Print and PDF export use the Numeral setting (#3784) (#3824)

* Fix .doc bookmark table desync on names longer than 40 characters (#3949)

* Fix .doc bookmark table desync on names longer than 40 characters

Fixes #3690

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a re…

* Status bar names the page in view, not only the caret's page (#3880) (#3901)

* Inline shared Word 2003 headers, notes and comments once (#3985)

Fixes #3652

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* G14: Romanian: translate the 68 rows that carried English text; feminine stângă/dreaptă, unambiguous arrows and brackets (#3558)

The integration of #3203 filled the keys the catalog lacked with English text.
These are now translated (cm, mm and pi stay as they are). 24 labels take the
feminine stângă/dreaptă (Bordură stângă, Margine dreaptă), arrows read
"spre stânga/dreapta" and brackets say which bracket, so "paranteză dreaptă"
(square bracket) never meets dreapta (right).


Claude-Session: https://claude.ai/code/session_01JadgGo4G2GHaAMNb3qyygk

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Keep inline content and formatting in Convert Text to Table (#3937)

Fixes #3751

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Compare: don't let a character-level diff scramble a reordered digit run (#3224)

lcs_ops is a textbook LCS, which finds the mathematically longest common
subsequence, not the most readable edit script. On two short strings that
happen to share a few characters in a different order (very common with
dates, IDs, phone numbers), it faithfully threads a chain of single-character
matches through what a reader expects to be one clean replacement — e.g.
"01.09.2026" -> "19.10.2026" came out as an alternating run of tiny
deletions and insertions instead of deleting "01.09" and inserting "19.10".

Add collapse_noise_matches, called only for character-level compares: it
folds a short (length-1) match that sits strictly inside a run of edits back
into the surrounding delete/insert, flattening the region into one clean
delete followed by one clean insert. A match that opens or closes the whole
sequence is the genuine common prefix/suffix lcs_ops already stripped before
running its table, so it's never touched regardless of length, and the fold
is gated on character granularity only — a matched whole word at word-level
compare is essentially never a coincidence and must survive.

Fixes #3094.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>

* Regression tests for #2 and #504 (#3860)

* #2: test that style gallery labels are truncated and hanging previews stay in view

Long custom style names were painted at full width and ran into neighbouring tiles, and a style with a hanging indent larger than its left indent previewed with its first line off the tile's left edge. The tests render the Styles gallery with one such style: the label must be no wider than its 74-point tile and elided, and the preview's text must start at the tile's left edge and stay whole. Without the fix the label measures 291 points and the preview's ink spans only columns 0..24 (cut by the edge).

* #504: test that the Backstage side menu edge is not a resize handle

The File side menu is a fixed 200-point panel, but egui left it resizable, so hovering its right edge drew a resize line and showed a resize cursor that did nothing. The test hovers the edge and checks the pointer is not a resize cursor. Without the fix it reports ResizeEast.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>
Co-authored-by: dfgodvan <dfgodvan@gmail.com>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Michael Higgins <55243596+Higgs32584@users.noreply.github.com>
Co-authored-by: In Mean <6434741+inmean@users.noreply.github.com>
Co-authored-by: Jayesh Prajapati <156836276+Jayesh-Prajapati9@users.noreply.github.com>
Co-authored-by: Gabriel Matozo <69991354+GabrielMatozo@users.noreply.github.com>
Co-authored-by: Mathieu Rancourt <40959576+MatRanc@users.noreply.github.com>
Co-authored-by: pakiat <49967840+pakiat@users.noreply.github.com>
Co-authored-by: Marco Arnulfo <103679419+marcoarnulfo@users.noreply.github.com>
Co-authored-by: Menonro <nitayd2001@gmail.com>
Co-authored-by: Gabriel Luís da Silva <79342117+mercuryVM@users.noreply.github.com>
Co-authored-by: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com>
Co-authored-by: dhruvLearner8 <83899022+dhruvLearner8@users.noreply.github.com>
Co-authored-by: vioreltus <142961713+vioreltus@users.noreply.github.com>
2026-10-23 12:48:58 -04:00
Michael MuandClaude Sonnet 5 02e71f2b33 Compare: don't let a character-level diff scramble a reordered digit run (#3224)
lcs_ops is a textbook LCS, which finds the mathematically longest common
subsequence, not the most readable edit script. On two short strings that
happen to share a few characters in a different order (very common with
dates, IDs, phone numbers), it faithfully threads a chain of single-character
matches through what a reader expects to be one clean replacement — e.g.
"01.09.2026" -> "19.10.2026" came out as an alternating run of tiny
deletions and insertions instead of deleting "01.09" and inserting "19.10".

Add collapse_noise_matches, called only for character-level compares: it
folds a short (length-1) match that sits strictly inside a run of edits back
into the surrounding delete/insert, flattening the region into one clean
delete followed by one clean insert. A match that opens or closes the whole
sequence is the genuine common prefix/suffix lcs_ops already stripped before
running its table, so it's never touched regardless of length, and the fold
is gated on character granularity only — a matched whole word at word-level
compare is essentially never a coincidence and must survive.

Fixes #3094.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-23 12:37:25 -04:00
NetterSilverandClaude Opus 5.5 dbee608dca Keep inline content and formatting in Convert Text to Table (#3937)
Fixes #3751

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:37:16 -04:00
Federico Olivares 84e7237275 Merge origin/main into spanish-proofing
Resolve the ROADMAP.md conflict: the status line keeps both changes, and main's
revision-history row comes before the Spanish proofing one.
2026-10-23 13:20:42 -03:00
MarekSpodBiedry 4571cd58c1 Print and PDF export use the Numeral setting (#3784) (#3824) 2026-10-23 12:17:45 -04:00
Federico Olivares 35cdfcd770 Merge origin/main into spanish-proofing
Resolve the ROADMAP.md conflict: main's progress-log entries come first, then
the Spanish proofing entry, and the status line keeps both changes.
2026-10-23 12:49:42 -03:00
NetterSilverandClaude Haiku 5.5 1ce8d3e598 Refuse arrange.group when members exceed the group child cap (#3958)
Fixes #3679

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-23 11:45:11 -04:00
+10 4045d76052 Validate PNG extension in file.exportPng (#3837)
* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* changes to filter dialogue

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* Parse the Flat OPC part root tag once when inheriting namespaces (#3944)

* Parse the Flat OPC part root tag once when inheriting namespaces

Fixes #3727

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take (#3816)

* Restrict Editing: a file command that changes the document is checked too

`always_allowed` waved through every `file.*` id, so on a protected document
the file commands that do change it went unchecked: the privacy options and
the properties wrote document settings, and the inspector's removals deleted
content. The privacy options' own comment already said they were "refused on
a protected document"; that wasn't true.

The allow-list now names the file commands that don't change the open
document, and the ones that do are checked like any other command. Reading
the document stays allowed: the inspector without removals, the privacy
settings, the info, and saving a version snapshot.

Restoring a stored version replaces the whole document, and a snapshot's own
protection is stale — restoring one lifted the protection and dropped its
password. The live document's protection, password and formatting limit now
survive a restore.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the user name can't change under protection

`file.setAuthor` sets the name a protected document's named exception
ranges are checked against, and the author new revisions and comments are
recorded under. `always_allowed` waved through every `file.*` id, so on a
protected document the name could be changed: an agent could take the
identity of a named editor and unlock their exception range, and forge the
authorship of the revisions it leaves behind.

A protected document refuses the change now, with the rule Word uses for a
content control's properties: stop protection first. The check is in the
command itself — `file.setAuthor` is a pure command, so `check_before`
never sees it — and in `check_before` for the day it isn't.

Word lets the user name change under protection: it is a preference there.
This project doesn't, because that identity gates what may be edited.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: a protected document's protection needs an explicit stop

The guard in `restrict` refused a protected document only when its
protection had a password, so a password-less one could have its
protection changed — and `file.protect {"mode": "none"}` stopped it
outright. An agent could strip a user's protection silently and then edit
the document freely.

Two rules now, as Word has them (Stop Protection first):

- The protection of a protected document doesn't change, whatever the
  mode. The formatting-only protection doesn't count: it stores the mode
  "none", and it limits the styles, not the editing.
- Stopping the protection is explicit. The "none" mode is the shorthand
  for it, but on a protected document only `{"stop": true}` stops it — the
  way the pane's Stop Protection button does it — so a command that
  merely names a mode can't strip it.

The password still guards the stop, and the formatting-only path (the
mode "none" with the formatting restriction) still works.

`cmd::tools::tests::protection_blocks_edits` stops the protection with
`{"mode": "none"}`; it has to stop it explicitly now.

Co-Authored-By: opencode <noreply@opencode.ai>

* Restrict Editing: the post-check compares the settings and the metadata

`outside_unchanged` is the check that rolls a command back when it
changed anything outside the editable regions. It compared the text, the
formatting, the page setup, the document defaults and the styles — but
not the document's settings, its metadata, its custom properties, the
resolved flag of its comments, its list definitions or its bibliography
sources. A command that changed the metadata of a protected document was
accepted and never rolled back: `file.properties {"title": "Forged
title"}` on a protected document with an exception changed the title and
stayed.

The settings, the core properties, the custom properties, the comments'
resolved flags, the list definitions and the sources are compared now, so
a command that changes them is rolled back like any other change outside
the editable regions.

The revisions and the media store stay out of it on purpose: an edit
inside an exception legitimately leaves a revision, and a picture
inserted there adds to the media store. The list definitions and the
sources are compared like the styles: the ones the document had must be
unchanged, but a new one may be added — a list or a citation inserted
inside an editable region adds its definition, like applying a style adds
one.

Saving is unaffected: it writes the metadata, but `file.save` is a pure
command and isn't post-checked, and the protection itself is changed by
`review.restrict`, which is always allowed.

Co-Authored-By: opencode <noreply@opencode.ai>

* The protection is stopped explicitly, and the docs say so

The test that stopped the protection used the "none" shorthand, which a
protected document now refuses; it stops it the explicit way instead, and
pins that the shorthand doesn't stop it. `file.protect` says the same: it
starts enforcement, and "none" doesn't stop a protected document.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the Restrict Editing gaps that are closed

Co-Authored-By: opencode <noreply@opencode.ai>

* A password guards the protection whatever its mode

The guard that stops the protection of a protected document from changing
asked whether the editing was restricted, so a formatting-only protection —
the mode "none", which limits the styles rather than the editing — was
exempted. A formatting-only protection can carry a password, and that password
guarded it: it was exempt from the guard, so `{"mode": "readOnly", "password":
"other"}` replaced the protection and the password with ones of the caller's
choosing, and the protection could then be stopped with them.

A password guards the protection whatever its mode, as it did before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The CLI's --author names the session, it doesn't change the document

`--author` dispatched `file.setAuthor`, which a protected document refuses
now that the name is checked as the identity its exceptions are matched
against — so a script that named itself on a protected file failed before it
ran a single command.

The flag names the session, not the open document, so it sets the name
directly, with the same normalisation and blank-name check the command uses.
`file.setAuthor` keeps its protection check for the callers that mean to
change the document's identity.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the progress docs' status lines carry today's date

Co-Authored-By: opencode <noreply@opencode.ai>

* The document's properties and a version restore need the protection stopped

Two gaps the review found, both of them a change to a protected document that
went through without the protection seeing it.

The properties (`file.properties`) and the privacy options
(`file.privacyOptions`) are document-wide state, and they aren't tracked: under
tracked changes they changed the document with no revision to show for it, and
under the other modes they were only rolled back by the post-check, after the
command had run. They are refused now, before the command runs, as the content
control's properties are.

A version restore (`file.versions`) replaces the whole document, including the
content the protection kept read-only, and it did that without the password:
`file.versions` is a pure command, so the protection never saw it. Keeping the
live protection and its password stopped them being dropped, but the content
underneath was still replaced. A restore needs the protection stopped first,
and the password with it when the document has one.

The live protection still wins over the snapshot's stale one, so a restore
can't lock the user out of their own document either.

Co-Authored-By: opencode <noreply@opencode.ai>

* Layout: keep with next takes the 0.01 pt of slack the other fit checks take

place_para_in_flow moved a paragraph kept with the next one to a new page
when the two needed more than the room left by any amount, while lines, rows
and floats fit with 0.01 pt to spare. A kept paragraph whose need came to a
rounding error over the page bottom moved on alone.

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* Engine: no dead-code warnings on wasm (#3542)

* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>

* Drop U+FFFE and U+FFFF from ODT export text (#3817)

Found while testing crates/formats/src/odt.rs:49-63 (fn x, the ODT text escaper; filter at line 58)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the number of VML path commands parsed from a path attribute (#3951)

Fixes #3683

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Return None from parse_number when the value overflows f64 (#3577)

Found while testing crates/doc/src/locale_number.rs:104

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Fix spurious 零 after a group's trailing zeros in Chinese numerals (#3745)

Found while testing crates/doc/src/cjk_numbers.rs:47-55 (Counting::format)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>

* Reject signs and non-digits in parse_date fields (#3575)

Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* #3816: also assert the next paragraph's first line stays on page 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>

* #3944: drop the wall-clock assert from the long-root-tag test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>
Co-authored-by: dfgodvan <dfgodvan@gmail.com>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>

* Write a text-box story shared by many shapes only once on docx save (#3983)

Fixes #3653

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Keep pending formatting revisions when Style Inspector clears formatting (#3971)

Fixes #3671

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

* Cap the headless ui_requests queue and keep one Paste Special request (#3997)

Fixes #3641

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Michael Mu <69072633+MichaelMuClaude@users.noreply.github.com>
Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
Co-authored-by: Joseph Anglada <fluorine@users.noreply.github.com>
Co-authored-by: dfgodvan <dfgodvan@gmail.com>
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
2026-10-23 11:06:17 -04:00
NetterSilverandClaude Haiku 5.5 1272cebf30 Cap the headless ui_requests queue and keep one Paste Special request (#3997)
Fixes #3641

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-23 11:04:03 -04:00
NetterSilverandClaude Haiku 5.5 6f484970a9 Keep pending formatting revisions when Style Inspector clears formatting (#3971)
Fixes #3671

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-23 10:56:06 -04:00
NetterSilverandClaude Opus 5.5 2d2123d251 Reject signs and non-digits in parse_date fields (#3575)
Found while testing crates/engine/src/cmd/controls.rs:113-123 (parse_date)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:32:06 -04:00
Joseph AngladaandClaude Sonnet 5.5 17bacb9a63 picture.corrections: a tiny sharpen value no longer panics in the blur (#3646) (#3830)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-23 10:31:56 -04:00
+5 462ecc8770 Engine: no dead-code warnings on wasm (#3542)
* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 10:25:42 -04:00
Federico Olivares 8e1bdf3898 #3360: user dictionary and Ignore All use the composed key spelling uses
Maintainer review on #3562: spelling looks words up composed (NFC), but
add_word, remove_word, ignore_word, custom_word (so loading the dictionary
file) and save_user_dictionary's removed list kept the raw lower-cased
spelling. A word added or ignored decomposed (`zqprue\u{301}ba`) therefore
stayed flagged.

- dictionary_key(): lower case, then composed; every write uses it, and
  in_user_dictionary() looks up composed, so either form adds, ignores,
  removes and finds the same entry; NFD entries in an existing file load
  and save composed
- English's known_core() reuses in_user_dictionary() instead of a copy of it
- issue byte ranges still cover the text as typed
- regressions: proof (add, Ignore All, remove, save/load with an NFD entry,
  both forms rechecked) and engine (review.addToDictionary and
  review.ignoreAll with the issue's NFD text)
2026-10-23 10:31:35 -03:00
Federico Olivares 56b0ff7760 #3360: decomposed accents stay in their word; URLs aren't Spanish questions
Maintainer review on #3562:
- words() keeps combining marks with the word they follow, so a word typed
  decomposed (NFD, `tambie\u{301}n`) is one word with its own byte range;
  is_correct_in_with and suggest_in look it up composed (NFC), so it is
  checked like `también` in every language (unicode-normalization, MIT/Apache)
- the Spanish missing-¿/¡ rule skips marks in web and e-mail addresses
  (`Ver https://example.com/?`), and punctuation inside an address or a word
  (`example.com`, `https:`, `10:30`) no longer starts the sentence the
  opening mark is offered for; a mark ending an address still ends a sentence
- Spanish repeated-word check matches composed and decomposed spellings
- regressions: check_text with the Spanish fixture and NFC/NFD equivalents
  (acceptance and ranges), words() and composed(), URLs ending in /? beside
  ordinary questions

CodeRabbit review: Session::region_spanish takes the ISO 639-2 `spa` as
Spanish, so a `spa-MX` region writes es-MX documents.
2026-10-23 08:43:44 -03:00
Federico Olivares c464a78663 Merge remote-tracking branch 'origin/main' into spanish-proofing
# Conflicts:
#	ROADMAP.md
#	docs/gaps.md
2026-10-23 07:27:43 -03:00
skylar taylor-barrick debb125ddf #1327: Preserve file identity after rejected document replacement 2026-10-22 21:50:35 -06:00
skylar taylor-barrick 5220199b6c #1327: Keep snapshots with their document through Compare 2026-10-22 21:30:19 -06:00
skylar taylor-barrick 8280903f26 #3277: Test Inspector refusal without protection 2026-10-22 21:19:58 -06:00
Mat Strangeandopencode 706d6cc7ef A comparison clears the version snapshots of the document it replaces
`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>
2026-10-22 21:19:58 -06:00
Mat Strangeandopencode 23c8dfa974 A version snapshot belongs to the document it was taken from
`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>
2026-10-22 21:19:58 -06:00
Mat Strangeandopencode 080d2caeb0 The inspector's tracking refusal doesn't cite an unrelated issue
#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>
2026-10-22 21:19:58 -06:00
Mat Strangeandopencode f87ab00a61 The Document Inspector refuses its removals while the document tracks changes
The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>
2026-10-22 21:19:58 -06:00
Mat Strangeandopencode ec1b539395 The inspector's removals are refused in every protected mode
`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>
2026-10-22 23:51:19 +01:00
Mat Strangeandopencode 5151809e11 Merge origin/main into fix/protection-file-commands
Resolves the conflicts with the changes that landed since the branch was cut:

- crates/engine/src/cmd/protect.rs: the forms-mode refusals stay behind
  upstream's `greyed_out()` (so the ribbon still greys the Developer tab's
  control buttons out), and the refusal of the document's properties and its
  privacy options is added on top of it.
- ROADMAP.md, docs/file-format-parity.md, docs/gaps.md: both sides' entries
  are kept; the status lines carry the merge's date and this change at the
  front.

Co-Authored-By: opencode <noreply@opencode.ai>
2026-10-22 23:08:09 +01:00
Federico Olivares eac1c2e81e Merge origin/main into spanish-proofing
Resolve conflicts with German hyphenation (ProofLang::De), Russian and Czech
in installed_dictionaries(), and #2164's U+FE0F flag fix: affix.rs takes
main's fix and drops this branch's duplicate. The Danish, Dutch, Romanian and
Slovenian catalogs gain the Spanish (Latin America) and (Uruguay) names; the
language menu tests get room for the four Spanish entries.
2026-10-22 02:46:30 -03:00
Federico Olivares 40494a5ca7 #3360, #3553: Spanish proofing (es-ES, es-MX, es-419, es-UY)
Spanish text was never checked: ProofLang had no Spanish variant, so every
es-* run fell through to Other (no marks). And the RLA-ES dictionaries
LibreOffice ships were refused by the affix engine for their `☎️` flag
(☎ + variation selector U+FE0F), which Hunspell reads as ☎.

- ProofLang::Es (Spain, plain es, spa), EsMx (es-MX), Es419 (es-419 and
  the other countries of the Americas) and EsUy (es-UY), chosen by region
  like the English dialects
- each variety reads its own installed Hunspell dictionary with fallbacks
  (es_ES; es_MX; es_419/es_US/es_MX; es_UY/es_AR); nothing bundled
- variation selectors in affix flag fields belong to the flag before them
- Uruguayan vos forms accepted when the dictionary has the infinitive:
  imperatives (vení, andá), with pronouns (decime, decímelo), present
  (tenés); a vos form typed without its accent gets it back first
- suggestions use the dictionary-driven Czech ranking (prueva → prueba,
  corector → corrector, tambien → también)
- grammar: repeated word, spacing, missing opening ¿/¡, space after an
  opening mark, with Spanish messages
- Review › Language, status bar, Language dialog and File › Options ›
  Proofing list the four varieties; the two new names translated in every
  catalog; a Spanish interface writes new documents in the region's Spanish
- the dictionaries-folder engine test holds the Italian and Spanish
  fixtures, so parallel proofing tests no longer lose their dictionaries
- tests use hand-written synthetic dictionaries; engine end-to-end tests
2026-10-22 01:53:37 -03:00
aiongg 2060f86e8e Merge PR #3139: Fix #3137: Cannot insert a tab inside table cells (missing Ctrl+Tab shortcut)
docs/typing-parity.md: status entry prepended to main's line.
2026-10-22 01:54:35 +00:00
aiongg c354162712 Merge PR #3056: Insert Table of Figures: a dialog with the caption label (Table, Equation, your own) (#3013)
ROADMAP.md: the PR's status entry prepended to main's first status line. i18n catalogs: union by key.
2026-10-22 01:51:21 +00:00