* Engine: the Word 2003 format check reads through io, so commands still never touch files themselves
is_word_2003_file (#1081) opened the file with std::fs, which the file-access guard test
(commands_do_not_touch_the_file_system_themselves) forbids outside io.rs; main failed it.
A new io::read_file_head reads the first bytes inside the read root.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* UI: a chrome click counts the page as focused at once, so the next keystroke isn't dropped; the ribbon-focus test passes again
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: DOCのDIB画像変換でバイト上限を守る
* Drop the duplicate read_file_head left by the merge with main
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: haranishi <223786623+haranishi@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
review.deleteComment removed anchors from the body only, so a comment
anchored in a header, footer, note or text box left its CommentStart and
CommentEnd behind, pointing at a deleted definition. It also collected only
direct replies, so a reply to a reply survived with its parent gone.
Anchors are now removed from every story, and the deleted set is the whole
thread below the comment. The thread is collected from a parent-to-replies
map built once, with a seen set, so a parent cycle from a hostile file ends
and a long reply chain doesn't take quadratic time. Undo restores everything.
Regression tests: a header comment (anchors checked in the header story), a
three-level reply chain (with its text parts and Undo), a parent cycle, and a
50,000-reply chain.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Save got focus only when nothing else had it, but the page keeps focus while typing and claims Tab and the arrow keys, so the prompt opened without a focused button and the keys moved the caret behind it. Save now takes the focus whenever none of the prompt's buttons has it.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The model's paragraph walk (each_para / each_para_mut) stopped 16 tables
down, while a .docx opens with tables nested up to 24 deep and pasting can
nest them further. Text box pruning, which runs after every edit, took a
box shown only from deeper for one shown nowhere and dropped its story,
so an unrelated edit lost the box's text. The same walk decides which
boxes a copy takes along and which pasted shapes get stories of their
own, and serves reading order, the next drawing id and note conversion.
The walk now keeps its own stack instead of recursing, so it reaches
every paragraph however deep tables nest, in the same document order and
with no call stack to run out of; a paragraph on its own still needs no
allocation. The signature is unchanged (the depth argument is now
unused), so every caller builds as before.
Tests: a box 17 tables down in an opened .docx survives an unrelated
edit and a save, while a box shown nowhere still goes; boxes 17 and 40
tables down, in a box and in a header are kept, counted in reading order,
and new drawing ids go above the deep shape's; a copied deep table
pastes into another document showing its own box's text.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The Word XML Document (Flat OPC) reader counted only the parts it kept:
a repeated or nameless part, a child of the package that isn't a part
and anything nested inside one were read without being counted, nesting
had no cap, and a part's inline XML was copied before its size was
checked. Every child of the package now counts against the part limit
and every part's data against the part and package size limits, kept or
not; nesting and the elements of each child are capped as the XML tree
reader caps a part's; and data is checked before it is copied.
Base64 in pkg:binaryData was taken from plain text only, so CDATA and
character references were dropped and a picture written that way lost
bytes. Both now read as the same text, and an entity there is an
invalid base64 error.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
russianLower/russianUpper showed 1), 2), 3). They now follow Word's 29-letter sequence (no ё, й, ъ or ь; ы kept), then repeat the letter (аа, бб, ааа), as observed black-box in Word.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The story tables of footnotes and endnotes (PlcffndTxt, PlcfendTxt),
headers and footers (Plcfhdd) and text boxes (PlcftxbxTxt) were read
one range at a time, and each range only had to run forwards: a table
stepping back to an earlier CP after every story pointed many notes,
header slots or text boxes at the same text, and each was read into a
part of its own (2,000 note references on one story made 1,001 copies
of it). The CPs of a well-formed PLC ascend, so a story that starts
before the end of the last one kept is now dropped (a header or footer
slot is left empty, which links it to the previous section's): every
subdocument CP becomes at most one story, and the stories together
never hold more than the file's text.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* A file name that isn't valid Unicode no longer crashes wordcraft-cli or the app (#1440)
Both binaries read std::env::args(), which panics on a non-Unicode
argument such as a Latin-1 file name. The CLI now reads args_os(): the
file a command opens keeps its real name and opens (convert, info, text,
inspect, render, zotero, run --file), and any other such argument is an
error naming it, exit 1. The app leaves such a file out with a warning,
as it does a file that fails to open, since file.open takes its path as
text; a --control port that isn't valid Unicode exits 2.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* The app opens a file whose name isn't valid Unicode too (#1440)
file.open takes its path as text but also accepts the file's bytes
under a name, as dropped files use. The app reads such a file, opens
its bytes under the lossy name and keeps the real path on the session,
so Save writes back to it. A unit test covers the open and the save.
The CLI test now checks that every command in OPENS_FILE reaches its
open with the file, and no longer pins `pos` counting a `--page` value.
ROADMAP says which saves go back to the file.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #1440: skip non-UTF-8 file name tests on macOS
APFS refuses file names that aren't valid UTF-8, so the fixtures fail
with "Illegal byte sequence" there, and the app-start test keeps running
because macOS opens a window without a display variable. The unit test
for the CLI's argument checks and the control-port test still run.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Proofing: status bar flags a language whose dictionary isn't installed (#1167)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* vi: missing-dictionary status strings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
On macOS five of Word for Mac's default shortcuts were missing or unreachable:
- Cmd+Shift+S is Save As (it opened Apply Styles, which keeps Ctrl+Shift+S
on Windows and Linux).
- Cmd+Option+E inserts an endnote. macOS takes Cmd+Option+D to show and
hide the Dock, so the old key never reached the app.
- Cmd+Option+A adds a comment (Cmd+Option+M still works).
- Control+H opens Find and Replace (Cmd+H still works).
- Cmd+Option+R collapses or expands the ribbon (ui.collapseRibbon).
Keys on Windows, Linux and the web are unchanged. Tests pin the bindings
on each platform.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* contributors: add Nael-Nathanael
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* insert.picture and insert.icon leave the caret after the picture (#965)
A script that typed right after inserting a picture replaced it, so the
picture was missing from the saved file. Commands now leave the caret
after the new picture; Picture Format commands still act on it. The app
still selects a picture the user inserts, as in Word.
Fixes#965
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Select a pasted picture again, keep pictures inserted in a row (#965)
Desktop paste inserts through execute, so after the caret change it
left a pasted picture unselected. WordApp::select_inserted_picture
selects the picture just before the caret; run and the paste path call
it, paste once after the last picture so a multi-file paste keeps them
all. Tests for paste selection and for two inserts in a row.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.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>
* #1336: Markdown looks up the backtick run closing a code span instead of rescanning the text
Code spans and link brackets searched the rest of the text for the run
of backticks closing a code span. Every run nothing closes (one per
length) cost a scan to the end, and the bracket scan repeated those
searches for each `[` over them, outside its look-ahead budget: 2 MB of
such runs took 2-18 s. The runs are now indexed by length once per text
and found with a binary search; what reads as a code span or a link is
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1452: Markdown brackets no longer turn later links in the paragraph into plain text
The link and image bracket scans share a look-ahead budget per text, so
hostile input stays linear, but it was charged more than they read: a
`[` nothing closes took the whole budget, and every bracket pair and
every link took the rest of the text, as if a destination had been read
to the end. An interval like `[0, 1)`, a dozen references in a long
paragraph or ten links were enough to make every later link plain text.
A scan now pays for the bytes it reads and records the pairs it passes,
and the brackets still open at the end of the text, so later brackets
are looked up. A destination is read, and paid for, only when `(`
follows the `]`; one that parses costs its own length.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Crop handles: Picture Format › Crop crop mode on the canvas, picture.crop takes the new size
ui.crop toggles crop mode for the selected (unturned) picture: black crop
handles trim a side or two, keeping the scale and the visible part in place;
the cropped-away part shows dimmed; Esc, Enter, a click outside or Crop again
keep the crop. A drop is one picture.crop with width/height, one undo step;
a floating picture moves by what a left/top crop trims.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Crop handles: flipped pictures trim the side under the handle, stale drags drop unapplied
Review round 1: the crop maths, the dimmed preview and the floating
position shift map the on-screen side to the source side (flip H swaps
left/right, flip V top/bottom). A drag remembers its picture and is
dropped unapplied if that picture is no longer the one in crop mode.
Sizes stay within 1-4000 pt at the same scale. Pointer-path tests drive
a real handle drag and a click outside. The dim is darker, and the
canvas keeps following a crop drag's pointer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Docs after rebasing on main
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Crop handles: ui-parity row merged with main; #1242's Cell Size note moved to the Tables row
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>
* DOCX: draw VML preset shapes such as Word's right arrow (#1377)
A VML v:shape that takes its geometry from a preset shapetype
(type="#_x0000_t13", o:spt) was dropped on import. Its preset number now
comes from the shape's o:spt, its v:shapetype, or Word's _x0000_tNN id,
and the presets whose geometry matches a DrawingML preset WordCraft draws
are read with their fill, outline and size (paint left out by the shape
comes from its shapetype): rectangle, rounded rectangle, ellipse,
diamond, isosceles triangle, five-point star, right arrow, line and
straight connector, heart, text box, and the flowchart process, decision
and connector. Saving writes them as DrawingML presets. Text in a preset
keeps the shape around it. MSOSPT numbers from [MS-ODRAW] 2.4.24.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* DOCX: draw unmatched VML presets as their nearest shape (#1377)
VML preset numbers now map to the DrawingML preset of the same shape and
go through preset_kind, so a preset WordCraft has no shape for (left, up
and down arrows, right triangle, callouts, four-point star...) is drawn
as the nearest one, as DrawingML presets are, instead of being left out.
The exact mappings are unchanged. WordArt text shapes, picture frames
without a picture and non-primitive shapes are still not drawn.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #1376: a word too wide for the strip beside a floating object moves on whole
Text in the narrow strip beside a square-wrapped picture was split mid-word
("docu" | "mentazione") when the strip was wider than the 20pt minimum but
narrower than the word. A span narrowed by a floating object is now used only
if the line's first word fits it; otherwise the word tries the row's next span,
or goes below the object. A word wider than the whole line still breaks in
place, and the 20pt minimum measured for floating tables is unchanged.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #1376: a line after a skipped strip starts its row; word width skips hyphenation points
A line placed in a row's second span after the first was skipped was marked as
beside a line before it, so line numbering and pagination treated it as the
second half of a row. The word measured against the full line now runs to the
next real break, not the first hyphenation point, and a row with nothing on it
jumps below an object that actually crosses the column.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* wordcraft --control checks its port (#1426)
--control took whatever followed it as the port and dropped it when it
didn't parse, so `--control --sample` lost --sample, `--control abc`
started without the control channel, and a bad value cleared
WORDCRAFT_CONTROL_PORT. A flag after --control is now left for the loop,
and a missing, non-numeric, zero or out-of-range port prints an error
naming it and exits with status 2, after the loop so --version still wins.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* wordcraft --control names any negative number it rejects (#1426)
A value after --control counts as the port when it starts with a digit
or a minus and a digit, so a negative too long for i64 is named in the
error instead of left in the file list. NonZeroU16 replaces the
separate zero check.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The index was a u16 clamped to u16::MAX while the style was still
appended, so every distinct format past the 65,536th in a paragraph
drew with the last one that fit: its font, size, link and tracked-change
marking. It is now a u32, like the glyph ranges beside it.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Over differently formatted paragraphs the dialog showed the caret
paragraph's values and OK gave them to every selected paragraph, through
para.rtl/ltr, para.align and para.set (three undo steps). It now reads
every selected paragraph: a field they share shows as before, a field
they differ in shows blank (number boxes empty, alignment empty, check
boxes neither on nor off, no direction picked) and OK leaves it as each
paragraph has it until the user sets it. OK sends only the fields that
changed, as a single para.set.
para.set takes "onPage": alignment and indents as seen on the page,
which each paragraph turns into its own start and end once `props` is
applied, so mixed directions get the right sides in one command. The
line spacing and space boxes no longer pull an out-of-range value from
the document (line spacing 6) into their range, which counted as an
edit.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
File Explorer's New › Microsoft Word Document makes a zero-byte .docx.
file.open read it as a Word package, failed, and left the untitled
document in place, so Save asked for a name. An empty file (from disk or
as bytes) now opens as the Normal-template blank document File › New
gives (region's paper, editing language) and keeps its path.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Insert › Link › Place in This Document: link.targets, links to bookmarks, headings and the top (#1234)
* Insert Hyperlink: Link to › Place in This Document (#1234)
The Insert Hyperlink dialog gets Word's "Link to:" choice. Place in This Document
lists the top of the document, its headings (indented by level) and its
bookmarks, and links to the chosen one; the text to display follows the choice
until it is typed over. It uses insert.link with `top`, `heading` or `bookmark`
and link.targets for the list.
* Docs: update the progress docs for this change
* Docs: put the new sentence in the WordCraft / evidence column
* i18n: Insert Hyperlink link-target strings 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>
* #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>
A picture with Wrap Text > Behind Text could not be dragged where it overlapped text, in the body or in a header/footer. Pressing it did nothing; once it was selected some other way (Selection Pane, arrow keys) the arrow keys moved it, but the mouse never started a drag. Reported from the field (#82).
Root cause: the hit test deliberately lets body text win over an object behind it (crates/layout/src/hit.rs: 'objects behind the text only where there's no text'), so objects::pointer never began a frame drag.
Fix: grab_at/pointer now treat a Behind Text picture, shape or group as a drag candidate even where text covers it, in body and header/footer stories. The press does not take the gesture at once: it becomes an object drag only after the pointer passes the drag threshold (pending_behind), so a plain click still places the text caret in the overlapping text. Text boxes are unchanged (their inside is their text; only their border grabs them).
Verified with three egui_kittest UI tests driving real pointer events, plus a headless ui_shot run against the real app:
- floating_picture_can_be_dragged_in_front_of_or_behind_text
- floating_header_picture_can_be_dragged_in_front_of_or_behind_text
- behind_picture_dragged_over_text_without_being_selected_first
Full wordcraft-ui-egui suite: 365 passed.
Docs: ROADMAP progress log and revision, docs/ui-parity.md, docs/gaps.md (#3).
Co-authored-by: Muthiullah Hib <thye911@mail.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #1077: forms protection fills content controls (w:edit="forms")
* #1077: forms walk stays conservative for unclosed control markers
* #1077: forms protection edges — nested locks, orphan markers, tables, empty interiors
* #1077: forms protection — formFields state and Find Next Region over fillable controls
* #1077: forms protection round-trip, password guard, docs
* #1077: final review fixes — lock-param gate, per-list pairing, docs, pane perf
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Insert › Bookmark: Delete and Go To, laid out as Word's (#1236)
The Bookmark dialog had only Add: a bookmark could not be deleted, and a
click in its list jumped to the bookmark at once. It is now laid out as
Word's (observed in Word 2019 on Windows): the name box over the list, Add,
Delete and Go To beside them, Sort by Name or Location and Hidden bookmarks
below. A click in the list picks a name; Add (Enter) adds and closes; Delete
and Go To leave the dialog open, and after a delete Cancel reads Close. The
dialog opens on the bookmark around the caret, and Add is off for a name
the command would refuse.
insert.deleteBookmark {name} removes a bookmark's start and end wherever
they are, also across paragraphs, as one undo step, keeping the selection
on its text; from the command search without a name it opens the dialog.
Adding a bookmark under a name in use goes through the same removal: it
looked for the old end only in the start's paragraph, so a bookmark across
paragraphs left a stray end behind. edit.goto with a bookmark selects its
text, as Word's Go To does, instead of placing the caret at its start.
Closes#1236
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Insert › Bookmark: Ctrl+click on a link to a bookmark keeps a caret (#1236)
Word's Go To selects a bookmark's text, but Ctrl+click on a link to a
bookmark only puts the caret at the bookmark's start (checked in Word 2019
on Windows). Following a link runs edit.goto, which now selects, so it
collapses the selection to its start.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Resolve the doc conflict markers left by the previous merge
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>
* Table Layout: the Cell Size boxes apply to the whole selection (#1242)
table.rowHeight and table.columnWidth read the caret's cell only. With the whole
table selected the caret is in the last cell, so only the last row's height and
the last column's width changed.
Both now use the cells the selection covers (selected_cells): every selected
row gets the height and every grid column the selected cells touch gets the
width, as one edit. Without a selection that spans cells they still change the
caret's row and column.
* Docs: update the progress docs for this change
* Docs: put the new sentence in the WordCraft / evidence column
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
eframe shrinks the start size to the monitor's, but not by the frame, the
title bar or the taskbar, and Windows places a new window away from the
screen's corner. On a laptop screen the first window's bottom, with the end
of the scroll bar, sat under the taskbar or past the screen's edge, so a
drag of the scroll bar stopped where the pointer met the edge. The window
was saved that size and opened so again on every launch.
On the frame that first knows the monitor, a window that doesn't fit with
48 pt to spare on each side (WindowGeometry::fit_on_monitor) gets a smaller
size, never below the minimum, and is centred. A window the system placed
counts its position too; a restored one, maybe on another monitor, only its
size. Maximized, full-screen and minimized windows are left alone. Checked
on Windows with no saved settings: the window now opens wholly on screen.
Closes#1215
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Windows Magnifier keeps the text cursor in view through UI Automation: the
caret's own box has no width, so it takes the box of the character after
the caret, and when that has none either, of the two characters before it.
The canvas described a line's end mark (the paragraph mark, a line break)
with no width, so on the empty line Enter or Shift+Enter makes Magnifier
looked at the end of the line above and stayed there until two more
letters were typed.
A line's end mark now takes the room a selection shows a paragraph mark
with: PARA_MARK_WIDTH (5 pt, now shared with selection_rects) at the line's
visual end (Line::mark_box). Checked live with Windows Magnifier, its view
read with MagGetFullscreenTransform: after Enter and Shift+Enter it moves
to the new line at once.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Cut copies to the system clipboard from every control
The ribbon, the context menu and the native menu's Cut deleted the
selection but wrote nothing to the system clipboard: only egui's Cmd+X
Copy/Cut event put the text there, in each UI caller that handled it.
Move the write into WordApp::run (execute), where edit.copy and edit.cut
already return the text, and drop the per-caller copies so nothing writes
twice.
Regression test: run edit.cut and edit.copy and check the text reaches
the context's clipboard output.
* Test Cut publishes to the clipboard from every entry point (#1265)
The one clipboard sink is WordApp::run; assert the ribbon button path
(run_from_ribbon) and a real Cut key event both reach it, alongside the
existing generic-dispatch test the context menu, native menu, command
search and CLI/MCP all go through.
* rustfmt
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>
Table Design and Table Layout were in the keytip tab list inside a table but had no letter, so no badge showed and no key reached them. They now take Word's two-letter badges, JT and JL: J waits for the second letter and narrows the tab badges to the J ones, then the tab's control badges show as on any other tab.
Co-authored-by: Brandon Thomas <echelon@gmail.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>
* Russian proofing: spelling, suggestions, grammar and word division
Runs tagged ru get Russian tools instead of none:
- spelling against an installed ru_RU Hunspell dictionary (not bundled;
LibreOffice dict-ru folders searched; KOI8-R and Windows-1251 decoded),
with ё optional, hyphenated words and solid compounds understood, не with
adjectives, participles and verbal nouns, prefixed verbs, abbreviations
with endings and camel case left alone, plus our supplement of modern
words with their declensions (assets/spelling/ru-extra.txt);
- suggestions tuned for Russian: е/ё, letter groups, common confusions,
missing spaces, typos and a stem scan;
- grammar rules with Russian messages (repeated words, spaces, sentence
capitals with Russian abbreviations, likely missing commas before
clause openers, doubled punctuation);
- Russian word division from the spelling rules, written as code.
* Options: Russian in the dictionaries list (ProofLang::Ru arm)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Магда М.Г. <mail@mgmagda.ru>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
docbin decrypts XOR-obfuscated, RC4 and RC4 CryptoAPI files after verifying the
password against the stored verifier; the engine reuses the .docx password
prompt errors. Written from [MS-DOC] and [MS-OFFCRYPTO]; synthetic tests only.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #778: .xls and legacy-encoding recipient lists; filter and sort recipients
- Recipient lists: legacy Excel 97-2003 .xls (BIFF8 reader on the cfb crate already in the
tree); Excel 5.0/95 and password-protected .xls refused with a clear message; a CSV named
.xls reads as text, an HTML one gets an error. .xls added to the picker filters (desktop,
web, built-in browser) and drop handling.
- Text lists (and plain-text import) detect legacy encodings when not UTF-8/UTF-16: GBK/GB18030,
Big5, Shift_JIS, EUC-KR, Windows code pages (encoding_rs + chardetng, MIT/Apache); an
`encoding` param overrides.
- mailings.filterRecipients / mailings.sortRecipients: field, comparison (equal, notEqual,
less, greater, lessOrEqual, greaterOrEqual, blank, notBlank, contains, notContains), value,
And/Or joins; sort keys. Preview, Finish & Merge and labels use only the matching records
in sort order. Edit Recipient List edits every record and keeps the filter.
- Recipient List dialog: Filter and Sort section and a merge-order column; catalog rows;
strings in every catalog; docs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: Hungarian and French strings for this change
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review fixes for #885: total order for recipient sort, Bulgarian strings
- compare_cells now ranks numbers (compared numerically) before text (compared
case-insensitively), so it is a total order; a column mixing 9, 10 and 1a
could make slice::sort_by panic. Regression test sorts mixed columns.
- Bulgarian catalog: the 24 Filter and Sort strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* vi: mail merge Filter and Sort strings
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>
* #775: STYLEREF fields in headers and footers show each page's result
Running headers/footers with STYLEREF (e.g. the current chapter title) are
evaluated per page at layout time (ECMA-376 §17.16.5.59): the page's first
paragraph of the style (\l: last), else the nearest before, else after.
Style names/ids, heading levels (STYLEREF 1), character styles, and the
\n \r \w \s \t \p switches. Body STYLEREF fields update with the other
fields. STYLEREF added to Insert › Quick Parts › Field with its options.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #775: STYLEREF error text in our own words
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review fixes for #882: translate the STYLEREF Field dialog strings
Adds the style picker label, the six STYLEREF switch labels and the
STYLEREF description to all 18 interface catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* STYLEREF: reuse the style index while the body and styles are unchanged
Headers with STYLEREF rebuilt and hashed the body's style index on every
layout pass (echelon's follow-up note on #882). The layout cache now keeps
the index with the body blocks it came from (shared, so an edited block is
a different pointer) and the styles/numbering hash, and rebuilds it only
when either changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* vi: STYLEREF field strings
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>
* test(engine): drop a needless drop() that clippy 1.97 rejects (drop_non_drop)
* Comments: a click inside a selected balloon moves the caret in its text (#1013)
- The canvas under the markup area also "contains" the pointer, so a press on the balloon counted as a click on the page and left it
- Presses over a balloon card now belong to the card, in both the deselect check and the document click in mouse()
- Test: press, drag and release inside the selected balloon; a click on the page still leaves it
* Comments: an option to show only the selected text's comment (#1055)
- Track Changes Options › Comments: "In the margin" (default, as in Word) or "Only for the selected text"
- The second mode takes comments out of the margin column; the comment under the caret, or the clicked one, floats beside the page, over its edge in a narrow window
- The floating card has its own foreground layer, so presses on it never reach the canvas
- review.trackingOptions takes commentDisplay: margin|selected
- comment_range_at_caret: the comment whose range holds the caret
- Strings in all catalogs; tests for the option, the helper and the card's clicks
* vi: comment display options
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>
* test(engine): drop a needless drop() that clippy 1.97 rejects (drop_non_drop)
* Comments: a click inside a selected balloon moves the caret in its text (#1013)
- The canvas under the markup area also "contains" the pointer, so a press on the balloon counted as a click on the page and left it
- Presses over a balloon card now belong to the card, in both the deselect check and the document click in mouse()
- Test: press, drag and release inside the selected balloon; a click on the page still leaves it
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* feat(ui): implement Ctrl+drag to copy objects on canvas
* feat(ui): implement Ctrl+drag to copy objects on canvas and update translations for "Copy to"
* vi: Copy to
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* rustfmt
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>
* test(engine): drop a needless drop() that clippy 1.97 rejects (drop_non_drop)
* Ribbon: a held press no longer scrolls the document (#1019)
- Double-click release paths in the canvas returned before ending the text drag
- The stale drag made any later press drive the edge auto-scroll
- A drag now ends once the button is up, wherever it was released
- Test: header double-click, then a held press on the ribbon
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>