With *Bold* and _italic_ as real formatting on, the opening mark is not
part of the word AutoCorrect reads, so typing `*teh end* ` gives bold
`The end ` instead of `teh end `. A sentence capital is decided from the
text before the mark.
insertColumnRight shrank the grid but left each cell's width at the old value,
so the page laid the cells out wider than the grid and the saved w:tcW disagreed
with it; the cells now take their grid columns' widths.
file.versions with a restore also saved when the restore was refused (protected
document, no such version, or a restore that is not a non-negative integer):
the restore is checked before anything is saved, a non-integer restore is
refused instead of being read as absent, and an index that does not fit in
usize names no version rather than wrapping to a small one on a 32-bit target.
AutoFormat As You Type gains "Built-in Heading styles", off by default,
as `headingStyles` on `tools.autocorrect` and a checkbox under "Apply as
you type" in the AutoCorrect dialog.
With it on, Enter in an empty paragraph right after a line of one to five
words that ends in a letter or digit makes that line Heading 1. One or two
tabs before the line make it Heading 2 or 3, and the tabs are removed. The
line must be a Normal paragraph of the main text outside a list. The caret
stays in the empty paragraph after the heading, and Undo right after
brings back the tabs and the Normal style while the Enter stays. With Track
Changes on, the tabs are marked deleted (or removed, when the same author
inserted them) and the style change is a tracked formatting change; a line
holding a tracked deletion stays as it is. A heading style locked by
formatting restrictions is not applied.
AutoFormat As You Type gains "Set left- and first-indent with tabs and
backspaces", off by default, as `tabIndent` on `tools.autocorrect` and a
checkbox under "Automatically as you type" in the AutoCorrect dialog.
With it on, Tab before the first line of a paragraph with text indents the
first line by one default tab stop, and Tab before a later line (after a
line break or where the text wraps) indents the whole paragraph. A second
Tab before an indented or hanging first line also indents the whole
paragraph. Backspace at the start of the paragraph takes the first-line
indent back, then the paragraph's own left indent one tab stop at a time.
An empty paragraph and Tab inside a line still take a tab character.
While formatting is limited to the document's styles, Tab and Backspace
work as with the option off.
After a `*word*` or `_word_` span is converted on a space, punctuation
or Enter, the word without its marks is AutoCorrected as before:
replacements, capitals, ordinals and links (`*teh* ` becomes bold
`The `). Undo right after still restores the marks in one step, and
after Enter the Enter stays. A marked web address becomes a link
without carrying its bold or italic into the text typed after it.
* chore: resolve SonarCloud maintainability issues
* fix: restore compiling refactors, revert behaviour changes, run cargo fmt
- fontdb::norm: keep the closure for filter (char::is_alphanumeric takes
self, not &self, so it does not fit Iterator::filter).
- story.rs: write_mark_rpr needs &mut self (rev_attrs, rpr_change).
- Revert the viewport meta change and the return 0 added to
workspace_version/sha256, which changed behaviour or masked failures.
- cargo fmt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Import Document in odt page_parts after dropping glob re-export
* fix: apply_kind_properties binds ContentControl; cell_ref keeps char closures
* fix(engine): pass case helpers by value (clippy needless borrows)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
AutoCorrect found the start of the word before the caret one byte after the
last white-space character. After a no-break space (two bytes in UTF-8) or an
ideographic space (three bytes) that offset is inside the character, so the
word could not be read and AutoCorrect left it as typed: no replacement, no
sentence capital, no ordinal superscript and no link.
The word now starts after the whole white-space character. A typing test
covers a replacement, a sentence capital and a web address after each of the
two spaces, and an ordinal after each.
AutoFormat As You Type has a new option, "*Bold* and _italic_ as real
formatting". When it is on and a word is finished by a space,
punctuation or Enter, text enclosed in asterisks becomes bold and text
enclosed in underscores becomes italic, for plain and complex-script
text, and the marks are removed. The opening mark has to start a word
and the enclosed text may not start or end with a space, so 2*3* and
file_name_ stay as typed, and so do marks around a picture, field or
content control.
Undo right after brings the marks back; after Enter, the Enter stays.
The paragraph Enter makes is formatted as after a plain Enter. With
Track Changes on, only marks the current author typed as tracked
insertions are converted, and nothing is converted while formatting is
limited to the document's styles.
The option is off by default. Scripts set it with "boldItalic" on
tools.autocorrect, and the AutoCorrect dialog shows it on the AutoFormat
As You Type tab.
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>
`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.
The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.
Co-Authored-By: opencode <noreply@opencode.ai>
#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.
Co-Authored-By: opencode <noreply@opencode.ai>
The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.
The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.
Co-Authored-By: opencode <noreply@opencode.ai>
Resolves the conflicts with the changes that landed since the branch was cut:
- crates/engine/src/cmd/protect.rs: the forms-mode refusals stay behind
upstream's `greyed_out()` (so the ribbon still greys the Developer tab's
control buttons out), and the refusal of the document's properties and its
privacy options is added on top of it.
- ROADMAP.md, docs/file-format-parity.md, docs/gaps.md: both sides' entries
are kept; the status lines carry the merge's date and this change at the
front.
Co-Authored-By: opencode <noreply@opencode.ai>
* #2852: personal templates in File › New, Save as Word Template
File › New lists the .dotx/.dotm files in the user templates folder under
Personal (a subfolder per category, search covers them); a click makes an
untitled document attached to the template. Save As › Save as Word Template
starts in that folder (<settings>/Templates, made on first use; File ›
Options › Advanced › File locations › User templates overrides it).
Engine: file.listTemplates (bounded, network folders refused, read root
applies), file.newFromTemplate sets the attached template, file.options
userTemplatesFolder, file.saveTemplate keeps a .dotm name. UI:
ui.saveAsTemplate; Save dialog requests carry a start folder.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr
* #2852: review — a personal template asks about unsaved changes; network templates folder refused before it's looked at; New lists the folder once per visit with Refresh; .dotx/.dotm labels alike
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>
* #1281: English dialects in proofing — en-GB, en-CA, en-AU… flag each other's spellings (#1045)
English was one proofing language: en-GB, en-CA and en-AU all proofed like
US English, and US documents accepted British spellings. Now ProofLang has
three Englishes, picked by the tag's region:
- En (en-US, other regions, no language): British spellings flagged
(colour → color).
- EnGb (en-GB, en-AU, en-NZ, en-IE, en-ZA, en-IN…): American spellings
flagged (color → colour, center → centre, analyze → analyse); -ize and
-ise both right (Oxford spelling).
- EnCa (en-CA): British -our/-re/-ence, American -ize, tire, program.
Data: assets/spelling/en-variants.txt, our own ~280 American/British
pairs with a policy per English (MIT OR Apache-2.0, ATTRIBUTION.md row).
Inflections by rule (crates/proof/src/en_variants.rs). A dialect's form
is right when the shared list knows it or its other spelling, so
"realisation" is right in en-GB. No system dictionary needed.
Suggestions put the dialect's own form first. Review › Language lists
English (Australia) and (Canada); the Language dialog adds India,
Ireland, New Zealand and South Africa (names in every catalog, not
native-reviewed). An English interface on an en-GB/en-CA/en-AU… region
writes new documents in that English. Options › Language shows the
document's language instead of a fixed "English (United States)".
Tests: per-dialect accept/flag/suggest, tag mapping, no-panic proptests,
engine end to end through review.language, DOCX w:lang round trip,
region-based editing language, language menu.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8veLveuUsJJm6UEcQPEuW
* #1281: review — pin the #40 words (judgement, defence, licence, programme, catalogue) in en-GB and en-US
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr
* #1281: review — harness picks every proofing language in Review › Language and checks it is ticked
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr
* #1281: review — pin more British spellings in en-GB (goitre, sceptre, enrol, fulfil, instalment, skilful, wilful, encyclopaedia…)
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>
In table.cellMargins, resolve existing margins from the table, its style, or defaults and only overwrite the sides explicitly provided in the command parameters.
Closes#3490
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 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>
`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>
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>
New parts, lists and comments took "largest id + 1": after an id of
u32::MAX (a .json model or a .docx can carry one) that overflowed, a
panic in debug builds and a wrap to 0 in release, and list.define's
saturating_add reused the taken id. free_id gives one past the largest,
or the lowest free id once that would overflow; try_add_part,
try_add_list and try_add_levels report ids running out and add nothing,
and the commands that add parts and lists use them.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #1290: read Word's VML text watermark from DOCX headers
Word's Design › Watermark is a VML shape with a v:textpath in the headers.
The reader dropped it, so screen and PDF showed no watermark. It now maps
it to the document's watermark model (text, font, fill colour,
semitransparent from v:fill opacity, diagonal when turned), which the
renderers already draw. Only Word's watermark shape (PowerPlusWaterMarkObject
id) or text centred on the page both ways counts; other header WordArt,
body WordArt and shapes inside text boxes are left alone. Writing it back
to DOCX is still missing (it was already lost on save).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1290: write the watermark to DOCX as a VML text shape in the headers
The DOCX writer dropped settings.watermark, so a watermark read from Word
(or set in WordCraft) was lost on save. Every header part now starts its
first paragraph with a VML shape (ECMA-376 Part 4) whose v:textpath carries
the text, font, fill colour, semitransparency (v:fill opacity) and diagonal
turn, centred on the margins behind the text, with the reader's
PowerPlusWaterMarkObject id so it reads back as the watermark, not a shape.
The shape type is our own: a straight path the text follows, fitted to the
box. Slots the pages show with no header of their own or linked from an
earlier section (default; first page with a title page; even pages with
even/odd headers) get a header holding only the watermark; linked sections
stay linked. A blank watermark isn't written. Check Compatibility no longer
warns about watermarks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1290: give the watermark's text path a top and a bottom edge so Word draws it
With a single line as the shape type's path, Word loaded the watermark as
WordArt (right text, size and turn) but drew nothing. Checked black-box in
Word: diagonal, horizontal and header-less documents now show it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1290: test the watermark through a Word-shaped file, table-only and even-page headers
Read → write → read of a file with Word's watermark keeps the watermark, the
headers' text and paragraphs and one shape per header; header WordArt that
isn't a watermark doesn't become one. A header holding only a table gains no
line; with odd and even pages an even header is added and later sections
stay linked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* DOCX: read VML freeforms (v:shape with a path) as freeform shapes
Legacy VML shapes with their own `path` (ECMA-376 Part 4), such as rule
lines in headers, were dropped on open and lost on save. They are now
read as freeforms: m/t/l/r/c/v/x/e commands in the coordsize/coordorigin
space, nf/ns, v:fill and v:stroke opacity; arcs drawn straight, formula
references read as 0. Saved as DrawingML custom geometry.
Tests: header rule read and round-tripped; hostile paths and coordinate
spaces stay finite and never panic.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* DOCX: VML freeforms whose path uses formulas aren't drawn
Formulas (@n, from v:formulas) aren't evaluated; reading them as 0 drew stray lines. Parity row states the fill-opacity limit (none at 0, otherwise opaque, as for DrawingML fills).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* DOCX: VML freeforms reuse the watermark's vml_fraction
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1290: move the VML freeform reader out of this PR
The three VML freeform commits (b7a6cc7, d8fa665, 945cbb4) aren't part of
#1290; they go to their own pull request so this one stays on the
watermark.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Word forms keep their legacy form fields and rows' skipped grid columns when saved (part of #1174)
* Tests: field initializers name the new form field
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M10: retain local crash recovery on current main
* i18n: Backstage Recover in every catalog
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>
* M10: enable document renaming and account settings
* no-mistakes(review): Allow case-only document renames without overwriting files
* no-mistakes(review): Keep unsaved rename as session file name, not Title
* no-mistakes(review): Keep AutoSave and Recent across renames; Compare clears name
* no-mistakes(document): Document file.rename write-root rule; dedupe rename gap note
* vi: Rename Document strings
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>