mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-27 20:07:19 -06:00
a1f8be2a4978a40de672185e4affc19aa27df1df
91
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4c8dc00fe6 | Merge upstream/main (#4184 craft-libs bump) into the batch | ||
|
|
ffa51443f2 |
Bump craft-print, craft-pdf, krilla and craft-i18n to craft-libs 1774443 (#4184)
* Bump craft-print, craft-pdf, krilla and craft-i18n to craft-libs 1774443 * PDF export writes link annotations only for safe targets (craft_pdf::write::uri_target) * Chart default category and series names use fmt_plain, so RTL interfaces put no isolate marks into the document |
||
|
|
39bf1a6c76 | Merge PR #4074: cu20we: fix box float paragraph origins and preserve shape line caps | ||
|
|
d7429d217a |
Pictures: one decoder checks the header against the budget, progressive JPEG coefficients included (#3644)
Document pictures now decode through wordcraft_geom::picture (behind a new `image` feature of the geom crate). It reads the header, refuses the picture before anything is allocated when the decode's peak would pass the budget, then decodes with the header's size and the budget as limits. The peak is the larger of three moments: the decoder's output beside what it holds while decoding, the output beside its RGBA8 conversion, and that beside a premultiplied copy. A progressive JPEG's decoder (zune-jpeg) holds an i16 for every coefficient of every component, in whole MCUs, for the whole picture; the SOF header gives that size. A 7900 x 7900 progressive JPEG without subsampling needs 9 bytes a pixel (561 MB), past the 512 MB budget, although two RGBA buffers (499 MB) fit. With the fix reverted, the test's header-only file of that size was decoded and the test process peaked at 542 MiB resident. - Render and PDF export ask the probe in their wrappers (decode_pixmap, load_image), so a picture past the budget gets the grey box or is left out, as before. - The crop dialog and every picture edit decoded with load_from_memory and looked at the size afterwards. They decode through the probe now, and so do wrap outlines and the RTF re-encode. - A picture edit has the drawing budget instead of an 80 M pixel cap checked after decoding. A picture past that budget (about 67 M RGBA pixels) can't be drawn either, so no edit that could be seen is lost. |
||
|
|
7cf9ba6826 |
cu20we: merge current upstream and complete function contracts
Merge upstream main at
|
||
|
+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
|
||
|
|
01d0c63451 |
Sonar S1612: use fn paths for simple method closures (#4103)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
cfa59f2c73 |
cu20we: preserve non-paragraph float origins and document cap contracts
Use the pre-spacing origin only for paragraph-relative box floats. Expand
regression coverage for line anchors and frameless page/margin fallbacks,
and document the cap conversion helpers and synthetic regression contracts.
Merge upstream main through
|
||
|
|
e56d516c45 |
cu20we: preserve shape line caps in DOCX and rendering
Carry flat, square and round outline caps through ShapeExtra, DrawingML and VML import, DOCX export, display items, raster strokes and PDF strokes. Keep older JSON documents default-flat and preserve WordCraft ink's tool-defined caps. Add JSON, DOCX/VML round-trip, raster-pixel and PDF-operator regressions. Record the verified coverage and remaining theme/native Word checks in the parity docs. Co-Authored-By: GPT-6 <codex@openai.com> |
||
|
|
4571cd58c1 | Print and PDF export use the Numeral setting (#3784) (#3824) | ||
|
|
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. |
||
|
|
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. |
||
|
|
e311bd410f |
Merge PR #3155: Text: scaled letters (w:w) are stretched, not spaced out
crates/pdf/src/tests.rs: both sides' new tests kept. |
||
|
|
57205e310a |
Merge PR #2392: PDF export: lam-alef drawn as two glyphs at the same x extracts in reading order (#2310)
ROADMAP.md: main's status lines, with this change added to the newest. docs/file-format-parity.md: main's status line with this change added; PDF export row keeps main's outline note and adds the lam-alef note. |
||
|
|
9c3df446ff |
Merge PR #3247: #1899: a picture cropped to a sliver is drawn at the size the file gives
crates/metafile/src/lib.rs: re-export both TextLayout (#1915) and crop_window. ROADMAP.md, docs/layout-parity.md: main's status lines kept; the PR's log rows added. |
||
|
|
2df0803aea |
Merge PR #2099: fix(pdf): shapes with a description are figures in tagged PDF
crates/layout/src/display.rs: main's Paint path fill and fill.as_ref() with the PR's Option<GraphicKind> figure and described-shape figure. crates/pdf/src/lib.rs: main's holds_picture and rotated_pictures arm kept beside the PR's figure_alt. crates/pdf/src/tests.rs: union; the PR's shape test builds its fill with Fill::solid (main's shape fill type). |
||
|
|
54d7597afc |
Merge PR #3157: Borders: dotted, dashed and dash-dot borders take the reference app's dash patterns
crates/layout/src/display.rs: border_stroke maps Dotted, Dashed and DotDash to the PR's Border* strokes and keeps main's Triple; Stroke::dashes lists Triple as undashed. crates/pdf/src/tests.rs: kept both new tests. |
||
|
|
05c34486f5 | Merge PR #2086: fix(export): pictures marked decorative are skipped by screen readers | ||
|
|
81fbf9fb77 | Merge PR #3322: wasm: use the browser clock only on wasm32-unknown-unknown, not on WASI | ||
|
|
4da4ced7ce | Merge PR #1915: Draw EMF text records (EXTTEXTOUTW) in metafile pictures | ||
|
|
7f38f41d8c |
Use chrono instead of hand-rolled UTC conversion (#3427)
* Use chrono for UTC/calendar conversion instead of hand-rolled copies Replaces the hand-rolled civil_from_days / days_from_civil copies and the Excel serial-date, FILETIME and logging/PDF date conversions with chrono as a workspace dependency (default-features = false, std only, so wasm is unaffected). Output formats are unchanged and pinned by the existing (and one new) test. Excel's 1900 leap-year bug and the 1900/1904 systems keep their explicit special cases; a small proleptic-Gregorian fallback covers years outside chrono's NaiveDate range so no input changes behaviour or panics. AGENTS.md now says time goes through chrono. Follow-up to storytold/cadcraft#1785 * Never panic or reshape date output for clocks outside chrono's range chrono's From<SystemTime> unwraps, so a clock past year 262143 panicked inside the logger. logging::timestamp now uses the fallible DateTime::from_timestamp and reads such a clock as the epoch, as the old saturating code did. iso_from_unix_secs fell back to the Unix epoch for timestamps chrono rejects; it now keeps the proleptic Gregorian date civil_from_days gives, restoring the pre-chrono result for supported out-of-range u64 inputs. Years past 9999 are written with {:04} rather than %Y (which prepends a '+') in logging::timestamp, iso_from_unix_secs, docbin::filetime_iso and odt::now_iso, so byte-for-byte output is preserved. filetime_iso gained a docstring; AGENTS.md's chrono bullet now notes the narrow out-of-range fallback. Tests cover each boundary. * Don't build an unrepresentable SystemTime in the far-future test UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows' SystemTime and panicked before timestamp() ran, failing the Windows test job. Build it with checked_add and only assert where it exists. * Route out-of-range dates through chrono's 400-year cycle, not hand-rolled copies The PR kept hand-rolled Hinnant `civil_fallback`/`days_from_civil_fallback` copies for dates outside chrono's ±262143-year range, and AGENTS.md carved a "narrow fallback" exception for them. That left calendar arithmetic outside chrono after all. Replace every copy with the Gregorian 400-year cycle (146_097 days): chrono converts the day/year within one cycle and whole cycles are added back via i128 (so i64::MIN/MAX can't overflow). This keeps chrono as the only calendar logic and reproduces the pre-chrono proleptic Gregorian result for every value: - engine cmd/mod.rs `civil_from_days` (reached by the system clock and by `iso_from_unix_secs` for a u64 seconds count past chrono's range). - engine cmd/controls.rs `days_from_civil` and `days_in_month` (`days_in_month` now uses `rem_euclid(400)` since month lengths repeat every 400 years). - formats sheet.rs `civil` and docx chart.rs `civil` (Excel serials). Outputs are unchanged: verified the cycle result equals the old formula over a wide range and at the extremes with a scratch chrono program. New tests pin accepted inputs past chrono's range on both sides of the epoch, through the public/lib entry points and the private converters, with values from the old formula. Removed the AGENTS.md "narrow fallback" exception. pdf/lib.rs `civil` is left as-is: its only caller is the live system clock and its year is clamped to 0..9999 by the PDF DateTime, so the epoch fallback past year 262143 is the accepted live-clock behavior, not a hand-rolled copy. Tested: cargo test -p wordcraft-engine -p wordcraft-formats -p wordcraft-docx; fmt --check; clippy --all-targets -D warnings. * logging/docbin/odt/pdf: keep extended-year dates, no epoch fallback FredApps (CHANGES_REQUESTED) showed `DateTime::from_timestamp(..).unwrap_or_default()` changes output: a timestamp the old code converted past chrono's year 262143 range now collapsed to 1970-01-01 instead of its real proleptic-Gregorian date. The same regression exists wherever the PR swapped a hand-rolled converter for the epoch fallback. Fix every such site to keep chrono as the only calendar logic via the 400-year Gregorian cycle (146_097 days): chrono converts the day within one cycle, whole cycles carry in the year (i128, saturated into i64). Verified the cycle result equals the old formula across a wide range and at the extremes. - apps/wordcraft/src/logging.rs `timestamp`: added `civil_from_days(u64)` (cycle), kept the pre-1970 epoch clamp (`duration_since`) and the milliseconds/time-of-day format exactly. Pins: UNIX_EPOCH + 10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z (FredApps' example, was 1970-...); the checked_add u64::MAX/2 s fixture now asserts 292277026596-12-04T15:30:07.000Z instead of the epoch. - crates/docbin/src/metadata.rs `filetime_iso`: restored a cycle-based `civil(i64)` instead of `from_timestamp(..)?` (which dropped the field past chrono's range). A u64 FILETIME caps at ~1.83e12 unix s (year 60056), inside chrono's range, so no reachable value changes; pinned the largest FILETIME (u64::MAX) -> 60056-05-28T05:36:10Z to lock the extended-year path. - crates/formats/src/odt.rs `now_iso`: reuse the crate's existing cycle converter `sheet::civil` (made pub(crate)) rather than the epoch fallback. - crates/pdf/src/lib.rs `civil`: cycle converter; year returned unclamped and the caller's existing PDF DateTime 0..=9999 year clamp is kept exactly, so month/day/time of a far-future clock survive. Pins: civil(10_000_000_000_000)=(318857,5,20,17,46,40), civil(-10_000_000_000_000)=(-314918,8,13,6,13,20), civil(253_402_300_800)=(10000,1,1,0,0,0). engine `cmd/mod.rs::iso_from_unix_secs` already uses the cycle fallback (last round); unchanged. * Don't build a year-318857 SystemTime unchecked in the logging test Windows' SystemTime only reaches about year 30828, so UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before timestamp() ran and failed the Windows test job. Build it with checked_add and assert where the platform can hold it. The year-10000 case fits everywhere and stays unconditional. * Name the 400-year range extension as the one exception to the time rule The rule said never to hand-roll calendar math, but the range fix (shifting by whole 400-year Gregorian cycles so dates past chrono's year range keep their old output) is cycle arithmetic, so the rule and the code disagreed. The rule now names that exception: the library still computes month, day and time, and only whole cycles are carried in the year. * Say the library computes month and day; time-of-day is plain arithmetic Narrows the range-extension wording: the date library computes the calendar fields, and splitting seconds into hours, minutes and seconds is ordinary arithmetic, which is what the code does. |
||
|
|
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> |
||
|
|
c02ceef673 |
Borders: triple borders draw as three lines on screen and in PDF (#1740) (#2210)
* #1740: triple borders draw as three lines on screen and in PDF Triple borders (tables, paragraphs, the ### AutoFormat line) were drawn as double lines. Layout now emits a Triple stroke; the renderer and the PDF exporter draw three thin lines across the line's direction, so vertical table borders get three side-by-side lines too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * #1740: rebase onto main — triple border strokes use the line's f64 ends Main now converts the line ends to f64 once and draws through a shared offset-line closure; the triple stroke uses both instead of dereferencing the shadowed values. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr * #1740: review — PDF triple-border test checks the three strokes are distinct and 3 widths apart 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> |
||
|
|
f4299ff644 |
wasm: use the browser clock only on wasm32-unknown-unknown, not on WASI
`now_ms` in layout, `now_unix` in engine, formats and pdf gated
`js_sys::Date::now()` on `target_arch = "wasm32"`, which also matches
wasm32-wasip1 and wasm32-wasip1-threads. There js-sys has no host to call,
so it panics ("cannot call wasm-bindgen imported functions on non-wasm
targets"), and since `layout()` times itself with `now_ms`, every document
laid out on WASI aborts. `std::time` works on WASI, so these now use the
browser clock only on wasm32-unknown-unknown, the gate wordcraft-docx
already uses.
|
||
|
|
8e799be97e |
#1899: wrap outlines and PDF bitmaps follow the same crop window
contour::picture_outline turned the crop into a pixel window on its own, so a sliver or overlapping crop gave an empty window and Tight/Through wrapping fell back to the frame while the renderers draw a 0.5% sliver. It now uses wordcraft_metafile::crop_window, with the upper pixel edges rounded up. A PDF test covers the bitmap and metafile paths of the export (CodeRabbit on #3247). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0cca2f7d21 |
#1899: a picture cropped to a sliver is drawn at the size the file gives
A crop that kept under 5% of a picture's axis was widened to 5%, so a header bar cropped to 2.7% of a page-sized WMF came out half as tall and the text box over it fell below it. The rasteriser, the PDF writer and the metafile placement now share wordcraft_metafile::crop_window, which keeps down to 0.1% of an axis and keeps junk crops finite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
a683260abf |
Borders: dotted, dashed and dash-dot borders take Word's dash patterns
Paragraph, cell and table borders were dashed, but not with Word's patterns: dots were one width on and two off (Word: one and one), dashes four on and two off (Word: three and one), and dash-dot was drawn as plain dashes. - layout: borders use their own strokes (`Stroke::BorderDotted`, `BorderDashed`, `BorderDotDash`); `Stroke::dashes` gives every pattern in line widths. Underlines and the other dotted lines keep theirs. - pdf, render: lines are stroked with `Stroke::dashes`. Checked against Word for Microsoft 365 (ink along each border, on synthetic documents): borders 0.5, 1 and 2.25 pt wide, dotted / dashed / dotDash, are 1-1, 3-1 and 3-1-1-1 widths in Word; before 1-2, 4-2 and 4-2; after as Word (2.25 pt dashes: 6.72 on / 2.28 off in both). Test: in the PDF, a dotted 1 pt border is dashed 1 / 1, a dashed 2 pt border 6 / 2, a dash-dot 1 pt border 3 / 1 / 1 / 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
94d938fa63 |
Text: scaled letters (w:w) are stretched, not spaced out
Character scale (`w:w`, Format > Font > Advanced > Scale) widens or
narrows the letters themselves. WordCraft multiplied the advances but
drew each glyph at its natural width, so text at 200 % looked letter-
spaced (pdftotext even split it into words) and text at 50 % overlapped.
- layout: `Draw::Glyphs` carries `x_scale` (1 = 100 %), the run's scale
for its text and its line-end hyphen (whose advance is scaled too);
list labels, tab leaders, ruby, equations and shape text keep 1.
- pdf: a scaled run is drawn under a horizontal scale from its first pen
position, the positions brought back so each glyph lands where the
layout put it; the text and its extraction are unchanged.
- render: each glyph's outline is stretched across from its pen
position.
Checked against Word for Microsoft 365, "HIHIH" in Arial 20 pt
(Liberation Sans here), right end of the word (pdftotext) and ink:
200 % Word 179.78, ink 73.92..176.52; before: split in three words,
ink to 163.68; after 179.74, ink 74.04..176.52
150 % Word 152.19; after 152.52
50 % Word 97.76; after 98.07
100 % unchanged
Test: in the PDF, letters at 200 % are drawn twice as wide and as high,
at pen positions twice as far apart; at 100 % unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
322b3d765c |
test(pdf): a described shape is exactly one Figure
The test checked that the description is there, so a Figure written twice would still pass. It now also counts the alt text entries. Suggested by the CodeRabbit review of this PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d16dd5cd72 |
fix(pdf): tag each picture in a rotated group on its own
A rotated inline group is drawn inside one rotation. Its tag was chosen for the whole group: an artifact as soon as one member was decorative, so a picture next to it lost its Figure and alt text; otherwise one Figure with the first picture's alt text. The group is still turned as a whole, and each picture in it is now tagged on its own: decorative ones as artifacts, the others as Figures with their own alt text. Found by the CodeRabbit review of this PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
73d159b6c9 |
fix(pdf): shapes with a description are figures in tagged PDF
A shape with a description (Alt Text) was exported as an artifact like any other shape, so screen readers skipped it and the description was lost: an arrow or an icon drawn with shapes that means something was invisible to them. A shape that has a description, is not marked decorative and shows no text is now a Figure with its description, inline (rotated too) and floating. Shapes without a description and decorative shapes stay artifacts. A text box keeps its frame as an artifact and its text read as paragraphs: its description would replace the text, so it isn't used. The rule lives in `shape_figure_alt`. Draw::Figure's `kind` becomes an Option (None for a shape), and Placed::Shape carries the description for floating shapes. Not covered: a group's own description (its members are placed one by one), and shapes inside a Drawing Canvas. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8664afe58f |
fix(export): pictures marked decorative are skipped by screen readers
A picture marked decorative (Alt Text > Mark as decorative, `adec:decorative` in DOCX) was still exported as content: - tagged PDF: a Figure with its alt text, or with "picture" when it had none, inline and floating alike. PDF/UA expects decorative content to be an artifact, so screen readers skip it. Draw::Image now carries the mark, and the exporter tags a decorative picture (inline, rotated or floating) as an artifact instead of a Figure. - HTML and Markdown (and ODT, which reads the same model): the description, which the document keeps when a picture is marked decorative, was written as its alt text. A decorative picture now exports with an empty alt text (alt="" in HTML), the WCAG way to say "skip this". Pictures not marked decorative are unchanged. Charts and diagrams are left as figures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c91c1793a3 |
#2108: File › Print scales pages to another paper size (#2699)
* #2108: File › Print scales pages to another paper size Scale to paper size on File › Print (Word: Pages per sheet › Scale to paper size): No Scaling or any Layout › Size paper (Letter, Legal, A4, …). Every page is scaled to the paper and centred, the paper turned to the page's orientation, in the preview, the print job and Save these pages as PDF. Native jobs also ask the spooler for that media. The document's page size is unchanged. New `paper` param on ui.print/ui.printSubmit and file.printPdf; unknown papers are errors. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * pdf: bookmark destinations from #2364 use the scaled Fit too --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
5977112a0d |
render, pdf: fix vertical double and wave border rendering (#2113)
Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
999e2ae896 | PDF tests read ActualText spans, so RTL extraction no longer depends on the installed Arabic font (#1773) (#1973) | ||
|
|
fc08b0e4f6 |
Fix PDF outline targets after leading page breaks (#2484)
Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
341f370368 |
fix(pdf): scale bookmark destination coordinates with page Fit and support case-insensitive bookmark lookup (#2364)
* fix(pdf): scale bookmark destination coordinates with page Fit and support case-insensitive bookmark lookup * cargo fmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3787af0a67 |
Rendering and PDF check image limits before allocating (#1321) (#2001)
* WIP for #1321 * Images: check size budgets before decoding and cap page rasters (#1321) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Images: charge decode peak, valid-CRC header tests, cap text-line rasters (#1321) Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
2e062168f0 |
Text effects: legacy outline, shadow, emboss and imprint are drawn (#2534) (#2548)
The legacy run properties w:outline, w:shadow, w:emboss and w:imprint were read, resolved and saved but never drawn. Layout now turns them into text effects (hollow outlined letters; a sharp grey copy down-right for shadow and emboss, up-left for imprint, as Word's PDFs show them) and draws them through the existing WordArt path, so screen and PDF both get them and the PDF keeps the text. WordArt w14 effects on the same run take precedence. Text drawn from outlines keeps synthetic bold (a widened outline, or one in a solid fill's colour and transparency). Colour glyphs (emoji, no outlines) and top-to-bottom runs with upright characters are drawn as plain glyphs, and tracked insertions and deletions keep their legacy effects. Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
da11d6634b |
Shapes: preserve DrawingML outline dashes (#2661)
* Shapes: preserve DrawingML outline dashes * Shapes: scale dashes to effective path width (#380) * Keep the outline dash with noFill; cap the raster dashed stroke width at 200 pt Addresses the two CodeRabbit review notes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Use clamp for the dashed stroke width (clippy) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fc15682fb0 | Preserve REF field hyperlinks in PDF exports (#2597) | ||
|
|
2ffdf353a1 |
#2310: PDF: lam-alef drawn as two glyphs at the same x extracts in reading order
Some fonts (the static Noto Naskh Arabic builds) draw لا as a lam and a zero-advance alef with their own clusters at one x. visual_order kept the tie in logical order, so the right-to-left run's ranges didn't decrease, krilla dropped /ReversedChars and السلام extracted as السالم. Ties between clusters now go in reverse text order when right to left (text order otherwise). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
df28b78de9 |
refactor(pdf): reconcile shared writer with current main (PDFW)
Refresh #1558 onto
|
||
|
|
7af9748ace |
M1: fields keep w:dirty through saves; computing a result clears it (#877) (#946)
A field begin marker flagged w:dirty by a producer that couldn't compute its result (pandoc-style TOC placeholders) lost the flag on save, so Word no longer refreshed the field on open. Mirror w:fldLock end to end: FieldState, InlineObject::Field/FieldStart, the reader (fldChar and fldSimple) and the writer (buffered, range and TOC-spanning begins). Update Field/Update Table clear it on the fields they compute, like Word's F9; untouched fields keep it. Round-trip and engine regression tests, incl. a pin for #878 (imported TOC placeholder replaced in place, fixed earlier by #617). Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6528f972db |
Shapes: draw flowchart OR spokes (#1644)
Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
c3ba9011a2 |
fix(pdf): preserve shared writer across main updates (PDFW)
Retain the shared writer and its regressions alongside the current PDF text behavior and the equivalent bounded find-pattern condition. PR #1558. |
||
|
|
6931b8db2a |
Draw EMF text records (EXTTEXTOUTW) in metafile pictures
Fixes #1764 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |