mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-27 11:57:36 -06:00
b400a26aaae6f8c68e1125f2cd98df5ac3cbfd8e
206
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
+39 |
12fc444bd7 |
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 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
|
||
|
|
a2312a310f |
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> |
||
|
|
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> |
||
|
+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> |
||
|
|
4e692b4123 |
#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. |
||
|
|
32f8e2c2ad |
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> |
||
|
|
66ff2afc4d |
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> |
||
|
|
5dc68481ed |
Integration: track t09
crates/ui-egui/src/backstage.rs: t03's bidi_display on the Recent names over t09's borrowed name/path. people.toml: union. Tests: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged. |
||
|
|
3581f80251 |
Integration: track t07
# Conflicts: # ROADMAP.md # docs/file-format-parity.md # docs/gaps.md # docs/layout-parity.md |
||
|
|
04b3881e3e |
Integration: track t06
docs/gaps.md row 4: one row (t06 had kept a second, older copy of it); t05's #1840 and t06's #2964 clauses both in, main's #2963 clause kept. docs/recovery.md: t06's retry sentence with t01's base64 size text. Test files: both tracks' tests kept whole. |
||
|
|
16d1c8a933 |
Integration: track t05
crates/pdf/src/lib.rs: t01's vector_text kept before t05's watermark (box area signature). Test files and canvas.rs tests: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged. |
||
|
|
0d3b8eb0cb |
Integration: track t04
crates/formats/src/odt.rs: t01's Body::new/take_blocks kept; take_blocks finishes tables with t04's TableB::finish (column widths). Test files: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged. |
||
|
|
245b5bd8d0 |
Integration: track t02
Docs: status lines combined (t02's entries first); log rows unioned; file-format-parity .rtf row from t02 (#1653, duplicate .rtf row dropped as t02 did), .odt row word-merged. |
||
|
|
406694ea58 | Integration: #3429 the based-on-chain test's doc comment back above its test | ||
|
|
ef00b55870 |
Merge PR #3429: #3415: banded rows shade the same rows in the reference app and WordCraft (table style band sizes)
crates/docx/tests/fixtures.rs: union, keeps main's shape-gradient test and the PR's band-size tests. ROADMAP.md, docs/file-format-parity.md: main's status lines with this change added to the newest; log rows from both sides in date order. |
||
|
|
61570835bc |
Merge PR #3156: Watermark: the text fills its shape's box, as the reference app's textpath does
crates/docx/src/read/story.rs: main's vml_opacity with the PR's box size. crates/pdf/src/tests.rs: both sides' tests kept; the glyph_frames helper both #3155 and #3156 add stays once. |
||
|
|
d985d0ecce |
Merge PR #3289: M15: fix Russian ё ordering and decomposed Cyrillic keys
ROADMAP.md: status line and revision history carry both sides. docs/gaps.md, docs/localization-parity.md: status lines carry both sides. |
||
|
|
9cd24539bd |
Merge PR #2549: #1653: RTF import decodes the East Asian multibyte code pages (932/936/949/950)
docs/file-format-parity.md: main's status line with this change added. |
||
|
|
264bfdaed3 |
Merge PR #3333: M15: correct Cuban and Peruvian Spanish decimal separators
ROADMAP.md, docs/gaps.md, docs/localization-parity.md: main's status lines kept; the PR's log rows and gaps note added. |
||
|
|
1c757ece6b |
Merge PR #3199: Signature lines stay signable after saving a .docx
ROADMAP.md: status line once (this change first, then main's); progress-log rows union in date order docs/file-format-parity.md: status line once; VML row merged word by word (main's #1500 fill text plus the PR's signature-line text) |
||
|
|
b0fb2b9386 |
Merge PR #2101: File: recovery copies and .json files store pictures as base64 (#586 follow-up)
ROADMAP.md: status line, progress log and revision history carry both sides. docs/file-format-parity.md: status line and change log carry both sides. |
||
|
|
558f6c85af |
Merge PR #2343: Fix numbered list levels that start at 0 or are overridden to 1
crates/doc/src/numbering.rs: kept #3417's per-(num, level) seen map and its single-level override; the override now sets that level's LevelFlags { started: false, first: Some(start) } from #2343 instead of start - 1 (the reviewed integration fix for #2343). |
||
|
|
eedcd7e98c |
Merge PR #2244: fix(docx, doc): support VML wrap side and case-insensitive float anchors
crates/docx/tests/fixtures.rs: both sides' appended tests kept. |
||
|
|
9eb0dba8d0 | Merge PR #3417: doc: a start override restarts only its own level | ||
|
|
5a8cb21978 | Merge PR #3315: Reject signs and repeated '#' in Rgb::parse | ||
|
|
53418631eb |
Merge PR #1739: Tables nest 24 deep at most, so pasting a table into its own cell can't build a document that crashes (#1596)
ROADMAP.md: main's status line kept; both progress-log lines added. docs/target-app-parity.md: main's status line and Tables row kept, #1596 sentence added to the row; log row added. |
||
|
|
8077310218 | Merge PR #3463: fix(docx): preserve East Asian language w:eastAsia on w:lang | ||
|
|
11f265b807 | Merge PR #3304: Map Windows-1255 byte 0xCA to U+05BA | ||
|
|
cbc3246c2c | Merge PR #2298: Comments: comments in headers, footers, notes and text boxes are listed, get balloons and are reached by Next/Previous (#2289) | ||
|
|
abd1c7fb36 | Merge PR #1760: Tables: rows with gridBefore/gridAfter keep their grid columns | ||
|
|
50ef30939f | Merge PR #2034: Floating objects are drawn in their stacking order (relativeHeight) | ||
|
|
6b01aa967f |
Shape fills: gradients and transparency (#1500) (#1605)
* #1500: shape fills keep gradients and transparency (model, DOCX read/write) A shape's, text box's and canvas's fill is now a `Fill` (solid with transparency, or a gradient kept as the file states it) instead of one opaque colour, so gradients and transparency survive opening and saving. - doc: `fill::Fill`/`Gradient` with sanitizing for hostile input; `.json` keeps writing opaque solid fills as `[r,g,b]` and reads both forms; `ShapeExtra::outline_fill` keeps an outline gradient. - docx read: `a:solidFill` with `a:alpha`, `a:gradFill` (stops, `a:lin`, `a:path`/`fillToRect`, `tileRect`, `rotWithShape`, `flip`), theme fill styles through `a:fillRef` 1-999/1001+ with `phClr`, `a:grpFill`, `satMod`, outline gradients, VML `v:fill` gradients and opacity, canvas backgrounds. - docx write: `a:solidFill` + `a:alpha`, `a:gradFill` as read. - engine/docbin/layout: call sites; drawing still uses the main colour (next commit draws gradients). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * #1500: draw shape gradients and transparency (layout, render, PDF) - layout: WordArt's paint becomes a shared `display::Paint` (solid, linear, radial); `paint_for` lays a fill over the shape's frame (linear at its angle, `scaled` honoured; path gradients circular from their focus to the farthest corner). Shapes, freeforms and canvas backgrounds use it. - render: one `set_paint` for shapes, paths and WordArt; radial gradients; soft edges fade the real fill. - pdf: axial and radial shadings with per-stop opacity. - Review fixes: nested groups without a fill pass theirs on to `a:grpFill`, text-box contents don't take a group's fill, `phClr` transparency only on `phClr` colours, VML focus ends consistent and opacity clamped, `rotWithShape`/`scaled` written only when set, Shape Outline clears an outline gradient, shared stop sanitizing. - docs: file-format, layout parity, gaps #1, roadmap progress log. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * #1500: shape.fill sets transparency and gradients on every selected shape - shape.fill (now cmd/shape_fill.rs) takes {"color", "transparency"?} or {"gradient": {stops, type, angle, focus, rotateWithShape}}; a colour alone keeps the transparency; values are sanitized; applies to every selected shape, text box and canvas as one undo step. - Formatting (shape.fill, shape.effects) keeps a Shift+click multi-selection, as in Word. - view.formatShapePane toggles the Format Shape pane. - Session::undo_mark/merge_undo_since: a pane drag that applies as it goes undoes in one step. - Helpers for the pane: params(fill), stop add/remove/move, directions, variations and presets (variation and preset colours provisional until compared with Word). - Catalog: Shape Fill Gradient, Format Shape Pane. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * #1500: Shape Fill › Gradient and the Format Shape pane - Shape Fill › Gradient: No Gradient, Light and Dark Variations of the shape's colour, More Gradients… (opens the pane). - Format Shape pane (right): No fill / Solid fill (colour, transparency) / Gradient fill (presets, type, direction, angle, stops bar with add/remove/drag and arrow keys, per-stop colour, position, transparency, rotate with shape). Drags preview live, one undo step. - Right-click › Format Shape… on shapes, text boxes and canvases. - Gradient swatches have accessible names; new labels translated (own wording, clean room). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * #1500: preset gradients are our own design (clean room) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * #1500: Gradient variations, starting gradient and directions as Word 365 does them Observed in Word 365 (behaviour and values only): - Shape Fill › Gradient: a No Gradient swatch, then 13 Light and 13 Dark Variations in a grid five wide, in Word's order; light stops tinted 66/44.5/23.5% with saturation ×1.6, dark shaded 30/67.5/100% with ×1.15; linear ones scaled; corners with their tile rectangle. - Format Shape › Gradient fill starts from the theme's first accent (four stops, 0/74/83/100%). - Corner directions in the pane get the same tile rectangle; Add/Remove sit by the stops bar. - shape.fill carries "scaled" and "tile"; docx exports color_modified (lumMod, lumOff, tint, shade, satMod) so variations use the reader's own colour maths. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * #1500: review fixes for the shape fill ribbon controls - Gradient variations are built from the colour the shown variation came from, so picking variations in turn, or No Gradient, doesn't drift the colour. - A drag's undo mark is forgotten while the Format Shape pane is closed (a stale mark could merge later undo steps). - Dragging on the stops bar away from a handle no longer moves the selected stop; the arrow keys are taken by the bar (no focus moves or shape nudges). - Direction and Type changes keep a linear gradient's "scaled" and an imported tile rectangle; only a corner direction sets its tile. - Colour modifiers (lumMod, lumOff, tint, shade, satMod) live on Rgb in the doc crate, shared by the DOCX reader and the gradient builders. - Previews build their stop list once per swatch, not per vertex. - Translations for the pane's hint, direction and preset names. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * #1500: adapt to main: caret contrast samples gradient fills; upstream tests use Fill/Paint - Paint::at gives a paint's colour and opacity at a point; the caret's background sampling (#299) uses it for shape fills, now gradients or transparent, and the Format Shape previews use it too. - Upstream tests (#886 shapes survive saving, render) build fills as Fill/Paint. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * #1500: adapt to main after the rebase: VML presets, rounded corners, new catalogs - vml_fill takes the shape's shapetype: a VML preset shape (#1377) takes its fill, opacity and gradient from its shapetype when it states none of its own, as it already took its colour. - Rounded-rectangle corners (#1476) and the gradient paints meet in render, PDF and the caret's background sampling; ShapeExtra keeps both `corner` and `outline_fill`. - Tests that landed on main since build fills as Fill/Paint (VML presets, cover pages, VML freeforms, path arrowheads, PDF and render shapes). - The 40 Shape Fill / Format Shape strings in the hr, ko, sv and vi catalogs, which main gained since. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz * #1500: docs: shape gradients and transparency, Shape Fill › Gradient and the Format Shape pane Re-applied on main's docs after the rebase: progress log, file-format, layout and UI parity rows, gaps #1, status lines and revision history; parity checklist regenerated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz * #1500: a theme picture fill falls back on the reference colour; VML freeform opacity note Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz * #1500: adapt to main after the second rebase: VML shapetype tests compare Fill; parity checklist regenerated Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz * #1500: one VML fraction parser; a preset takes its gradient from its shapetype Review notes: story.rs's watermark/freeform opacity reuses shape_style's vml_fraction (clamped at the callers) instead of a second copy; a test pins a VML preset whose v:fill gradient comes from its v:shapetype, and the 65536ths form of a fraction. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz * #1500: adapt to main after the third rebase: the shape duplicate preview test fills with Fill Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz * #1500: review — harness test: right-click a shape, Format Shape… opens the pane and keeps the shape selected Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr * #1500: review — theme keeps every fill style a fillRef can reach (999 per list), still bounded Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: aiongg <aiongtaigi@gmail.com> |
||
|
|
a8ba49e68b |
fix(docx): preserve East Asian language w:eastAsia on w:lang
Preserve w:lang/@w:eastAsia when reading and writing DOCX documents. Previously, only w:val and w:bidi were handled, causing w:eastAsia to be dropped on save. Fixes #3457 |
||
|
|
67d21b0a68 |
#3415: banded rows shade the same rows in Word and WordCraft
Word reads a table style without w:tblStyleRowBandSize / w:tblStyleColBandSize as bands of zero and leaves its banded rows and columns unshaded (MS-OE376 §2.7.5.7, MS-OI29500 §17.7.6.5); ECMA-376 says one, which is what we did. Our writer only wrote a size a style set, and our built-in styles set none, so a table in Grid Table 4 saved by WordCraft had shaded rows here and none in Word. - An unset or zero band size now means no bands, as in Word. PartSet::of_cell takes row and column band sizes; column bands get a size of their own (col_band_size). - Our banded built-in styles and new table styles set both sizes to one, and the writer writes them (and zero), so Word bands them too. - The reader reads both sizes, zero included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
afbfa43351 |
doc: a start override restarts only its own level (ECMA-376 17.9.27)
A num's startOverride for a level applies when that level of that num is first used, and only there: applying all of them on the num's first use reset the shared counters' other levels and renumbered every following chapter. |
||
|
|
e717ae4dc5 |
fix(doc): use decimal points for Cuban and Peruvian Spanish
Add the two missing regional exceptions and regression coverage for parsing, grouped numbers, formatting, and Spain controls. |
||
|
|
dfb9e1a851 |
Reject signs and repeated '#' in Rgb::parse
Found while testing crates/doc/src/props.rs:49-56 (Rgb::parse) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
a31743eb6d |
Map Windows-1255 byte 0xCA to U+05BA
Found while testing crates/doc/src/encoding.rs:82 (CP1255 table, entry for byte 0xCA) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
3d03c7cc7a | M15: fix Russian ё ordering and decomposed Cyrillic collation | ||
|
|
fffd48c885 |
#2289: the first-page header comes before the default header in comment order
With Different First Page on, a section's default header was listed and reached by Next before its first-page header, so Next went to page 2 before page 1. A section's header and footer versions now go in the order of the first page each one shows on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
260014167d |
#2289: skip comments in header versions not in use and in linked text boxes' own text
A first-page or even-page header or footer is shown only while Different First Page or Different Odd & Even is on, and a text box linked from another shows the first box's text instead of its own. Comments in those stories were still listed and reached by Next and Previous, and typing then went into text nobody sees. shown_stories now picks each section's versions the way layout does and leaves out linked boxes' own text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2d645dba03 |
#1653: RTF import decodes the East Asian multibyte code pages
`\ansicpg932`/`936`/`949`/`950`, a font's `\fcharset128`/`129`/`134`/`136`, `\cpg932` and the OEM charset 255 were read one byte at a time, so Japanese, Korean and Chinese text came out as mojibake. The reader now holds the 8-bit text of a multibyte code page and decodes the run with encoding_rs (SHIFT_JIS, GBK, EUC_KR/UHC, BIG5): a lead byte and its trail byte decode together, escaped (`\'hh`) or raw. A code page switch (`\f`, `\fcharset`, `\cpg`, `\ansicpg`), a group end and the end of the document flush what is pending; invalid sequences read as U+FFFD, as Word writes them, and never lose the rest of the text. The pending buffer is capped at 4096 bytes. Single-byte code pages, `\uN?` escapes and their `\ucN` skips, the font table and every existing test are unchanged. Tests cover Shift-JIS (escaped, raw and through a `\fcharset128` font), CP949/CP936/CP950, a mixed Japanese/ASCII/ Cyrillic document whose code-page switch must flush, `\uN?` alongside multibyte text, an invalid sequence and a Windows-1252 regression. |
||
|
|
156817d6ba | Word signature lines stay signable after saving a .docx | ||
|
|
912b98c833 |
Watermark: the text fills its shape's box, as Word's textpath does
Word's text watermark is a VML WordArt shape (`PowerPlusWaterMarkObject`)
whose `v:textpath fitshape` stretches the text over the shape's box (its
`style` width and height), centred on the margins and, when diagonal,
turned by `rotation:315`. WordCraft ignored the box: it drew the text
70 % of the page diagonal wide (half of it on screen), centred on the
page and along the page diagonal (55° on A4, not 45°). A "BROUILLON"
watermark in a 515 x 104 pt box came out about 1.4 times as large as in
Word.
- doc: `Watermark::size`, the shape's box (`None`: Word's default, half
the page diagonal across, as high as the text's length makes it);
`Watermark::frame` gives the box for a page, shared by the writer and
the renderers.
- docx: the box is read from the shape's style and written back; a box
equal to the default for the document's page reads as `None`, so a
watermark made by Design > Watermark keeps following the page.
- pdf, render: the text's advance spans the box's width, its em the
box's height, the em's foot (the font's win descent below the
baseline) on the box's bottom; the box centred on the margins,
turned 45° counterclockwise when diagonal.
Checked against Word for Microsoft 365 (ink bounds, pt, left top right
bottom; fonts here: Liberation Sans for Arial and Calibri, Liberation
Serif for Times New Roman):
BROUILLON, Calibri, 515.4 x 104.1, 315°:
Word 93.6 222.7 489.1 619.2; before 60.5 110.4 535.7 732.5;
after 90.7 214.1 495.8 618.7
SPECIMEN, Arial, 400 x 100, 315°:
Word 136.3 261.6 455.0 580.3; before 66.7 108.5 539.0 727.7;
after 137.8 255.4 454.6 573.1
HXH, Arial, 300 x 100, flat: Word 158.9 378.2 436.3 449.8;
before 105.1 353.8 490.1 489.6; after 158.9 380.2 435.8 449.3
HXH, Times New Roman, 300 x 100, flat: Word 149.8 383.0 445.0 449.3;
after 151.2 383.5 443.5 449.3
COPIE, Calibri, 300 x 80, flat: Word 153.1 387.8 441.1 440.2;
before 96.0 373.9 500.2 469.0; after 152.2 387.4 443.5 444.5
Tests: a watermark with a box of its own is written and read back with
it, Word's default box reads back as none; in the PDF, the text of a
300 x 80 pt watermark is 80 pt high, stretched across, starting at the
box's left edge on the margins' centre line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
b8debb3050 |
Content controls: typed text takes the control's formatting, in any language (#3116)
* Content controls: typed text takes the control's formatting, in any language Text typed in place of a content control's placeholder takes the control's own run properties (w:sdtPr/w:rPr, ECMA-376 §17.5.2.27), as in Word: in a protected form filled in Word for Mac, all 17 entered values carry the style their control's rPr names. WordCraft kept that rPr only as XML to write back and gave typed text the placeholder's formatting minus the placeholder style. It also knew the placeholder style only by its English id, PlaceholderText. German Word writes Platzhaltertext (the name stays "Placeholder Text"), so in forms from German Word every entry stayed grey, on screen and in the saved file, and restoring a placeholder added a second Placeholder Text style. The style is now found by its built-in name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Content controls: typing drops a showing placeholder's own style A control without run properties that shows its placeholder: text typed over it drops the placeholder run's character style whatever its id and name. Word localizes only the placeholder style's id, but another program may localize its name too, and the name lookup then misses it. Restoring a placeholder and new controls keep the name lookup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
2304bf6ac6 |
Fix Paragraph::sentence_at at the first character of a sentence (#3105)
* Fix Paragraph::sentence_at at the first character of a sentence Found while testing crates/doc/src/para.rs:1397-1427 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * sentence_at: a terminator followed directly by text doesn't end the sentence at the caret "Hello.World" at offset 6 gave an empty (6,6) range; only treat the caret as just past a sentence end when the character at the caret is a space. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
517ba09250 |
Write large whole numbers without decimals in format_number (#3107)
Found while testing crates/doc/src/locale_number.rs:114 Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com> |
||
|
|
53296879db |
Floating objects are drawn in their stacking order (relativeHeight)
Word stacks floating objects by wp:anchor/@relativeHeight, not by the order of their anchors in the paragraph. The reader ignored the attribute, so a panel anchored after the text boxes it frames, but lower in the stack, was drawn over them and hid their text. - wordcraft-doc: Float gets a `z` (0 = none, drawn in document order). - wordcraft-docx: it is read from relativeHeight and written back as read; an object without one gets the next order above, as before. - wordcraft-layout: place_objects collects each object's items with its order and emits them lowest first (stable, so objects without an order keep the paragraph's). Four round-trip tests compare whole floats after a save; saving now gives objects without an order one, so those tests compare without it (`unstacked`). The order itself is covered by its own round-trip test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4b18b307be |
Selection rectangles walk only the paragraphs the range reaches (#2967)
Document::paths_between listed every paragraph of the story (every cell paragraph of every table) and then kept those between the two paths, so each selection_rects call cost the whole document. The canvas calls it every frame for each Find reading highlight (up to 5,000), each content control frame, Read Aloud and the hyperlink under the pointer. It now walks only the blocks, rows and cells between the two paths: for a one-paragraph range in a document of 26,000 paragraphs (four 1,500-row tables), 1.0 ms -> 2.6 us. Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com> |
||
|
|
80f6ba6a25 |
#2289: comment navigation follows the stories a reader sees, in reading order
Review of the first commit found, and ui_shot runs confirmed, three bugs in it: - Next Comment moved the caret into a removed header or a deleted footnote's text: a story nothing shows, so typing went nowhere visible. - Next visited headers and footers by part id (creation order), so a footer made first came before the header. - Comments in footnotes were claimed but untested. Document::shown_stories lists the stories a reader sees, in reading order: the body with the notes and text boxes it shows, then the headers and footers the sections use with theirs. It reuses the reachable walk that counted_stories uses. comment_list scans those stories (and returns at once when the document has no comments: the balloons ask every frame), and nav_comment orders by a story's place in that list instead of by Pos. tools::all_stories is private again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |