mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-09 04:01:39 +00:00
8a2a8bb60dfd46ddceee26a4aa91aeb5cbe14532
30
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
34511263a7 |
Customize Ribbon and Quick Access Toolbar (#379) (#384)
* Customize Keyboard dialog (#368) Tools > Customize Keyboard (tools.customizeKeyboard): pick a category and a command, see its keys, press a combination to see which command uses it, then Assign / Remove / Reset All. Also reachable from File > Options (Keyboard shortcuts: Customize...) and command search. - engine: KeyMap (assigned key -> command, removed built-in keys), consulted before the registry's built-in shortcuts; keys compared in one canonical form. Saved maps read leniently: junk entries and unknown commands are dropped, never failing the rest of the preferences. - tools.customizeKeyboard: with params (assign/remove/reset/command/key) it only changes or reports the map; without params it opens the dialog. - ui: key dispatch and menu/tooltip shortcut text use the user map; the map persists in ui.json; more keys (Q, F1-F12, 4/6/7, Insert) can be bound. - i18n strings in every catalog; docs/ui-parity, gaps #24, ROADMAP log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Customize Ribbon and Quick Access Toolbar (#379) Tools > Customize Ribbon (tools.customizeRibbon): one dialog with two pages. Ribbon: commands by category next to the ribbon's tabs; show or hide and reorder tabs, add custom tabs and custom groups (on built-in or custom tabs) holding any commands, rename, remove, reset. Quick Access Toolbar: add, remove and reorder its commands, reset. - engine: RibbonLayout (tab order and visibility, custom groups, the toolbar's commands). Saved layouts read leniently: junk, duplicates, reserved names and unknown commands are dropped. - tools.customizeRibbon: with params (hide/show, newTab, newGroup, add, remove, rename, move, qatAdd/qatRemove/qatMove, reset, layout) it only changes or reports the layout; without params it opens the dialog. - ui: the tab strip and ribbon follow the layout (custom groups draw the commands' own icons, else a lettered tile); the Quick Access Toolbar shows the user's commands; right-click any ribbon button to add it to (or remove it from) the toolbar or open the dialog; the toolbar's ... button opens a menu of common commands and More Commands... (#376); File > Options has a second Customize button; layout persists in ui.json under `ribbon`. - Open and Print icons drawn in code; i18n strings in every catalog; docs/ui-parity, gaps #24, ROADMAP log, parity checklist. Fixes #376. 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> |
||
|
|
9cf3b6862f |
Customize Keyboard dialog (#368) (#378)
Tools > Customize Keyboard (tools.customizeKeyboard): pick a category and a command, see its keys, press a combination to see which command uses it, then Assign / Remove / Reset All. Also reachable from File > Options (Keyboard shortcuts: Customize...) and command search. - engine: KeyMap (assigned key -> command, removed built-in keys), consulted before the registry's built-in shortcuts; keys compared in one canonical form. Saved maps read leniently: junk entries and unknown commands are dropped, never failing the rest of the preferences. - tools.customizeKeyboard: with params (assign/remove/reset/command/key) it only changes or reports the map; without params it opens the dialog. - ui: key dispatch and menu/tooltip shortcut text use the user map; the map persists in ui.json; more keys (Q, F1-F12, 4/6/7, Insert) can be bound. - i18n strings in every catalog; docs/ui-parity, gaps #24, ROADMAP log. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
adcbf52611 |
Move paragraphs, promote/demote, Go Back and Alt+X like Word (#343) (#351)
- para.moveUp / para.moveDown (Alt+Shift+Up/Down, Ctrl+Shift+Up/Down on macOS): the paragraphs the selection touches swap with their neighbour in the same story or cell, keeping style and list membership; selection follows; one undo step; refused at the container edges, next to a table and across a section break. - para.promote / para.demote (Alt+Shift+Left/Right, Ctrl+Shift+Left/Right on macOS): heading level up/down (Heading 1..9), promoted body text takes the level of the heading before it, list items change level like Shift+Tab/Tab. - edit.goBack (Shift+F5): cycles the caret through the last four edit places. - text.toggleUnicode (Alt+X; unbound on macOS): hex code <-> character, undoable. The canvas no longer types the key's letter after an Alt shortcut ran. - Catalog rows, translations in every interface catalog, typing/UI parity docs, tests. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
39ab3b5433 |
Field codes like Word's field keys (#339) (#349)
Show field codes instead of results with View > Field Codes (Alt+F9) or
per field with Toggle Field Codes (Shift+F9); a field showing its code is
drawn as { CODE } on light grey. Edit a field's code in a small popup
(double-click a field showing its code, right-click > Edit Field..., or
fields.setCode), and Ctrl+F9 asks for a new field's code. F9 updates
fields and shows the selected ones' results again.
New commands: fields.unlink (Ctrl+Shift+F9) replaces fields with their
text, fields.lock / fields.unlock (Ctrl+F11 / Ctrl+Shift+F11) keep a
result through updates (already saved as w:fldLock), fields.next /
fields.previous (F11 / Shift+F11) select the next or previous field, and
fields.selected reports the fields in the selection. Range fields
(citations) can be locked, unlinked, edited and stepped through too.
The right-click menu gains Update Field, Edit Field... and Toggle Field
Codes when the selection holds a field. Strings are translated in every
catalog; parity catalog, ui-parity, gaps and roadmap updated.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
|
||
|
|
5e84270e60 |
Hyperlinks cover only their own text (#413) (#449)
Linking a whole paragraph also linked its paragraph mark, and Enter copied the link at the split into the new paragraph's mark, so everything typed in the paragraphs Enter made (above or below) became the link, and hovering anywhere near the end of the text showed the link's tooltip. - doc: a paragraph mark never takes a hyperlink (format_range, split_off) - engine: typing at either end of a link isn't linked; inside it is (Word) - ui: hover tooltip and Ctrl+click only on the linked text itself Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
972df1e0c7 |
Track Changes: formatting revisions (w:rPrChange/w:pPrChange) (#41) (#399)
Formatting changes are tracked like insertions and deletions: - Model: runs and paragraph marks carry the formatting before a tracked change (CharProps::fmt_change), paragraphs their old properties (ParaProps::fmt_change) and a list numbering change; tables, rows, cells and sections their old properties. - DOCX: w:rPrChange (runs and paragraph marks), w:pPrChange, w:numberingChange, w:tblPrChange, w:trPrChange, w:tcPrChange and w:sectPrChange are read and written back, each last in its parent (ECMA-376 17.13.5). Nested changes are ignored; ids are renumbered. - Recording: with Track Changes and Track Formatting on, Session::run compares the paragraphs a command changed with the document before it, so every formatting command records without per-command code. An author's later changes keep the formatting before the first one; formatting set back to the original stops being a change. - Accept/Reject (at the caret, selection, all) keep or restore the formatting; Next/Previous and the Reviewing Pane list format changes with a "Formatted: Bold, Size: 14" description. - Markup: change bars beside formatted lines and "Formatted: ..." balloons in the markup area, per Track Changes Options. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e71e2d1c52 |
Draw tab: draw with pen, pencil and highlighter, erase ink, and hide ink (#315)
* Draw Table and Eraser: draw tables and split or merge cells with a pen (#303) Insert › Tables › Draw Table and Table Layout › Draw › Draw Table / Eraser. table.draw takes a pen stroke in page points: a rectangle outside a table inserts a one-cell table of that size at the paragraph under its start; a straight vertical or horizontal line inside a table splits the cells it crosses (new grid column or row; untouched cells span it). table.eraser merges the two cells beside the border nearest a point. Each stroke is one undo step. In the app the buttons toggle pen and eraser modes with a live preview, a crosshair with the tool's icon, and Esc to stop; canvas clicks don't move the caret while a tool is on. Splitting through part of a vertically merged cell and erasing beside one are refused with a clear error. Catalog 384/430 -> 387/430. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Draw tab: pen, pencil and highlighter ink, stroke eraser and Hide Ink (#307) Ink strokes are floating freeform shapes: a new Freeform geometry (paths in shape-local coordinates, opacity, and the pen that drew it) on InlineObject::Shape with ShapeKind::Freeform. draw.stroke inserts one anchored to the paragraph under the first point (one undo step; points capped per stroke, strokes capped per page, NaN dropped); draw.erase / draw.eraser with a point deletes the stroke under it; draw.pen, draw.pencil, draw.highlighter, draw.eraser and draw.select set the Draw tab's tool in the session view state; review.hideInk toggles showing ink on screen. The canvas draws a live stroke while a pen is dragged and commits it on release; the eraser deletes strokes it passes over (one undo step per drag); Esc or Select goes back to selecting. Draw tab buttons show the tool in use, with colour and thickness menus; Review gets an Ink group with Hide Ink. Ink renders with round caps and joins on screen and in PDF; highlighters are translucent and behind the text. DOCX writes freeforms as wps:wsp with a:custGeom (moveTo/lnTo) in a wp:anchor and reads them back, along with simple custom-geometry freeforms from other files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * #315 review fixes: eraser undo test, sanitized tap dots, custGeom path cap, ink docs - draw.eraser with a point erases through draw.erase: test that it is one undo step (undo/redo) and marks the document dirty. - Render and PDF: a single-point stroke's dot is drawn at the sanitized point, not the raw first one. - DOCX custGeom reading stops once MAX_PATHS paths (subpaths included) are collected, across a:path elements. - docs/file-format-parity.md: ink is saved as custom geometry, not InkML, so Word shows it as a freeform. - widgets test: theme::apply takes an Appearance (#317). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Draw tab: pen, pencil and highlighter ink, stroke eraser and Hide Ink (#307) Ink strokes are floating freeform shapes: a new Freeform geometry (paths in shape-local coordinates, opacity, and the pen that drew it) on InlineObject::Shape with ShapeKind::Freeform. draw.stroke inserts one anchored to the paragraph under the first point (one undo step; points capped per stroke, strokes capped per page, NaN dropped); draw.erase / draw.eraser with a point deletes the stroke under it; draw.pen, draw.pencil, draw.highlighter, draw.eraser and draw.select set the Draw tab's tool in the session view state; review.hideInk toggles showing ink on screen. The canvas draws a live stroke while a pen is dragged and commits it on release; the eraser deletes strokes it passes over (one undo step per drag); Esc or Select goes back to selecting. Draw tab buttons show the tool in use, with colour and thickness menus; Review gets an Ink group with Hide Ink. Ink renders with round caps and joins on screen and in PDF; highlighters are translucent and behind the text. DOCX writes freeforms as wps:wsp with a:custGeom (moveTo/lnTo) in a wp:anchor and reads them back, along with simple custom-geometry freeforms from other files. 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> |
||
|
|
1d7a54f883 |
Define New Multilevel List and Track Changes Options dialogs (#342)
* Define New Multilevel List and Track Changes Options dialogs (#328) Home > Paragraph > Multilevel List > Define New Multilevel List edits all nine levels with a live preview: number text and style, start, restart after a level, linked paragraph style, alignment, aligned-at and text indent, what follows the number with an optional tab stop, and the number's font. OK runs the new list.define command (list.get reads a definition back). List levels gain a restart-after level and a tab stop, which the counters and layout honour and .docx reads and writes (lvlRestart, num tab); number alignment now places the label. Review > Tracking's launcher opens Track Changes Options: show comments, ink, insertions and deletions, formatting; balloon mode; insertion and deletion marks and colours; changed-line bars and their colour; track formatting. They go through review.trackingOptions, are saved per user with the interface preferences, and the page drawing honours the marks, colours, hidden deletions, change bars and inline mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Parity checklist regenerated after merging main into #342 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> |
||
|
|
4b450f1b00 |
Open and save password-protected Word documents (#55) (#281)
* Clipboard pane: collect the last 24 copies and cuts and paste any of them again
Copy and Cut now also keep each item (rich fragment plus plain text) in a
session list of up to 24, newest first. Items over ~4 MB (estimated) are
skipped and the list stays under ~24 MB in total; copying the same thing
twice in a row adds it once. The system clipboard is untouched.
New commands: edit.clipboardPane (toggle the pane, also the Clipboard
group's dialog launcher), edit.clipboardItems (list with previews),
edit.pasteClipboardItem {index}, edit.pasteAllClipboard,
edit.deleteClipboardItem {index} and edit.clearClipboard. Pasting goes
through the normal paste path, so it is one undo step and respects Track
Changes. The pane lists the items with a short preview; clicking one pastes
it at the caret, and each has Delete, plus Paste All and Clear All.
Parity: 378 -> 379 of 428 features.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Layout > Arrange > Group: group pictures, shapes and text boxes into one object
Shift+click (or Ctrl+click) adds pictures, shapes and text boxes to the
selection; Group (arrange.group) combines the selected floating objects
into one group object that moves, wraps and resizes as one, with its
members keeping their places and scaling with the group. Ungroup
(arrange.ungroup) splits it back, where the group showed them, and
leaves them selected so Group puts them back together.
- Model: InlineObject::Group with the members' coordinate space (like
DrawingML a:chExt), so resizing the group scales its members.
- Layout draws the members inside the group's frame; hit testing
selects the group; text boxes in a group show their text.
- DOCX writes wpg:wgp with a:chOff/a:chExt and each member's a:xfrm,
and reads Word's groups (including inside mc:AlternateContent and
nested groups, flattened).
- select.addObject makes multi-selection scriptable (pos or Selection
Pane index).
Parity 378 -> 379 of 428 (88.6%); floor raised to 88.5%.
Fixes #265
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paragraph: Asian Typography settings, with kinsoku and mid-word Latin wrapping in layout (#269)
Paragraphs gain the Asian Typography flags (line-breaking rules, wrapping
Latin text in the middle of a word, hanging punctuation, compressing
punctuation at the start of a line, and automatic space between Asian and
Latin text and between Asian text and numbers). They inherit through styles,
round-trip through .docx pPr, are set by the new para.asianTypography
command and appear on a new Asian Typography tab of the Paragraph dialog.
Layout applies the kinsoku lists (no line starts with closing East Asian
punctuation or ends with opening punctuation), breaks between any East Asian
characters when kinsoku is off, and breaks Latin words anywhere when word
wrap is off. Text without East Asian characters breaks as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Manage Styles: list, preview, delete and show or hide styles
Home › Styles › Manage Styles (styles.manage) opens a dialog from the
Styles pane listing the document's paragraph, character, linked and table
styles, sorted as recommended or alphabetically, with a preview line and a
description (type, formatting it sets, what it is based on). From there:
Modify… and New Style… reuse the existing dialogs and come back, Delete
removes custom styles, and two checkboxes show the style in the Styles
gallery (w:qFormat) or hide it from style lists (w:semiHidden).
styles.manage returns the list for scripts (name, type, builtIn,
inGallery, hidden, basedOn, priority, own formatting). styles.delete now
moves paragraphs, runs and tables that used the style to its base (in
every story), rebases styles built on it and takes a custom linked
character half with it; built-in styles are refused and it is undoable.
New styles.setVisibility {style, gallery?, hidden?} sets the flags, which
round-trip through .docx.
Fixes #266
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Create and modify your own table styles, saved in the .docx (#146)
New Table Style and Modify Table Style (Table Design > Table Styles) make
real table styles in the document's style sheet: name, based on, and
formatting for the whole table, the header row and banded rows (borders,
fill, bold, font colour). The table at the caret takes the new style, it
shows in the gallery, and other tools that read table styles by name
(pandoc's custom-style, for one) see it.
- engine: table.newStyle / table.modifyStyle with documented params
(regions take borders, fill, bold, italic, colour, size, font; bandSize;
para); table.style only accepts table styles now.
- doc/layout: table styles gain header-row and row-band cell borders,
row-band character formatting and rows per band, applied by layout.
- docx: tblStyleRowBandSize and firstRow/band1Horz tcBorders and rPr are
read and written, so custom styles round-trip.
- ui: a compact dialog from the Styles menu next to the gallery; table
style tiles redraw when a style changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Table styles: every region, a live preview, delete, custom styles first (#146)
Follow-up to the custom table styles work: the New/Modify Table Style
dialog now covers every region a table style formats, shows what the
style will look like, and table styles can be deleted.
- engine: table.newStyle / table.modifyStyle take lastRow (total row),
firstColumn, lastColumn and bandedColumns regions with the same props
as the others. New table.deleteStyle removes a custom table style:
built-in ones are refused, styles based on it are re-based on its own
base with its formatting folded in (so they look the same), and tables
using it fall back to Table Grid. Undoable like any command.
- doc/layout: table styles gain fill and cell borders for the first and
last column and total row, and fill, text and borders for column
bands; layout applies them in the standard order (column bands, row
bands, first/last column, header/total row).
- docx: firstCol/lastCol/lastRow/band1Vert shading and cell borders are
read and written.
- ui: the dialog's "Apply formatting to" lists all seven regions and a
small live preview table shows the pending changes; Styles menu gets
Delete Table Style (disabled for built-ins); the gallery shows the
document's own table styles first.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paste pictures from the system clipboard (screenshots, Copy Image, copied files) (#45)
egui's paste event only carries text, so a clipboard holding just a picture
sent no paste at all on Ctrl+V, and the ribbon/context-menu Paste only knew
WordCraft's own copy ("the clipboard is empty").
Desktop: the app reads the clipboard's picture through a new
Services::clipboard_picture (arboard, already in the build via egui-winit):
copied files first, else image pixels. Mod+V whose release arrives without a
paste event, and Paste from the ribbon or menus, insert that picture through
insert.picture (one undo step); text naming copied picture files inserts the
pictures. Pixel sizes are hostile: at most 16,000 px a side and 50 Mpx, the
buffer must match, files at most 200 MB.
Web: a document paste listener reads image files from the paste event when
there's no text and delivers them through the inbox like dropped pictures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Shape Format: Shape Effects — shadow, glow and soft edges (#275)
Shapes and text boxes get an effects model (outer shadow with colour,
transparency, blur, distance and angle; glow with colour, size and
transparency; soft-edge radius), the shape.effects command (presets,
custom values or null per effect, applied to every selected shape,
undoable, hostile numbers clamped), screen and PDF rendering with the
same banded approximation of a blur, DOCX a:effectLst read/write
(outerShdw, glow, softEdge) with a matching wp:effectExtent, and a
contextual Shape Format tab with Shape Fill, Shape Outline and Shape
Effects (Shadow, Glow, Soft Edges submenus) plus Arrange.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Open and save password-protected Word documents (#55)
Opening a password-protected .docx used to fail as a broken zip and showed
a blank document. Encrypted packages (an OLE compound file with
EncryptionInfo + EncryptedPackage) are now detected and decrypted with
ECMA-376 agile encryption, written from the public [MS-OFFCRYPTO] spec:
password key derivation with spinCount, verifier check, data-integrity
HMAC, AES-CBC in 4096-byte segments (AES-128/192/256 with SHA-1/256/384/512
on read; AES-256 + SHA-512, spinCount 100000 on save). Standard (Office
2007) and certificate encryption give a clear "not supported" error.
- docx: crypt module (decrypt, encrypt, is_encrypted, read_with_password),
PasswordRequired / WrongPassword / Encryption errors; hostile files are
bounded (stream caps, spinCount <= 10,000,000, checked lengths).
- engine: file.open/file.save take "password"; file.encrypt
{"password": string|null} sets or removes the save password (pure, so it
never lands in Repeat or recorded macros); a document opened with a
password is saved with it again; file.info reports "encrypted".
Passwords are held in a zeroizing type whose Debug prints ***.
- ui: a password prompt when a user-opened file needs one (scripts get the
error), File > Info > Protect Document with Encrypt with Password (typed
twice) and Remove Password; web downloads are encrypted too.
- deps: aes, sha1, sha2, hmac, base64, getrandom (wasm_js in the browser),
zeroize.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Table styles: Table Grid and Normal Table can't be deleted even when a .docx marks them custom
A .docx can set w:customStyle on any style, which made Word's built-in table styles look custom
and deletable. table.deleteStyle and the ribbon's Delete Table Style now also recognise them by
id and name, so no table is left on a style that's gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Manage Styles: deleting a table style runs Table Design's table style deletion
styles.delete on a table style now calls table_style::delete (#263): its tables take Table Grid,
styles based on it keep their look, and Word's built-in table styles are refused even when a
.docx marks them custom (the Manage Styles list reports them built-in too). Document::restyle no
longer retargets tables' own styles, so there is one place that does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Paste pictures: Mod+Shift+V (Paste Formatting) no longer pastes a clipboard picture
The key-release fallback that pastes the system clipboard's picture treated every Mod+V release as
Paste. It now requires plain Mod+V or the Paste key (no Shift, no Alt), with a unit test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Asian typography: test that kinsoku keeps a manual line break before a closing bracket
Manual line breaks end the line by cluster kind, not by break opportunity, so kinsoku's filter
can't drop one before 」; pin that with a layout test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Encrypted saves: the data-integrity HMAC key is hashSize bytes
MS-OFFCRYPTO §2.3.4.14 makes the HMAC salt as long as the hash (64 bytes for SHA-512); it was
16. Reading already accepted both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: password dialog and File › Info strings in every catalog
Rows for the Password and Encrypt with Password dialogs, the Protect Document section of
File › Info and the password status messages in zh-hans, zh-hant, ja, pt-br, es, uk, sr and
sr-latn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Open and save password-protected Word documents (#55)
Opening a password-protected .docx used to fail as a broken zip and showed
a blank document. Encrypted packages (an OLE compound file with
EncryptionInfo + EncryptedPackage) are now detected and decrypted with
ECMA-376 agile encryption, written from the public [MS-OFFCRYPTO] spec:
password key derivation with spinCount, verifier check, data-integrity
HMAC, AES-CBC in 4096-byte segments (AES-128/192/256 with SHA-1/256/384/512
on read; AES-256 + SHA-512, spinCount 100000 on save). Standard (Office
2007) and certificate encryption give a clear "not supported" error.
- docx: crypt module (decrypt, encrypt, is_encrypted, read_with_password),
PasswordRequired / WrongPassword / Encryption errors; hostile files are
bounded (stream caps, spinCount <= 10,000,000, checked lengths).
- engine: file.open/file.save take "password"; file.encrypt
{"password": string|null} sets or removes the save password (pure, so it
never lands in Repeat or recorded macros); a document opened with a
password is saved with it again; file.info reports "encrypted".
Passwords are held in a zeroizing type whose Debug prints ***.
- ui: a password prompt when a user-opened file needs one (scripts get the
error), File > Info > Protect Document with Encrypt with Password (typed
twice) and Remove Password; web downloads are encrypted too.
- deps: aes, sha1, sha2, hmac, base64, getrandom (wasm_js in the browser),
zeroize.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Reland: catalog coverage 390/431 (90.5%) in ROADMAP and target-app parity
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Manage Styles: mark table styles with ⊞, which the interface fonts have (▦ has no glyph, #305's check)
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>
|
||
|
|
29e978d6d8 |
Re-land #264, #263, #270, #271, #267, #280: clipboard pane, table styles, Manage Styles, Asian Typography, Group, Shape Effects (#323)
The original squash merges of these six PRs were empty. Integrated and tested together on current main. Co-authored-by: Valentin Klein <vangel1976@googlemail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
88ad402581 |
Gridlines: the document's own grid spacing, and table gridlines on by default (#69, follow-up to #243) (#283)
View › Gridlines now draws the drawing grid at the document's own spacing: DOCX keeps it in settings.xml as w:drawingGridHorizontalSpacing and w:drawingGridVerticalSpacing (ECMA-376 §17.15.1.44/45), which WordCraft now reads (clamped) and writes (only when it differs from the default), with a round-trip test. Documents that don't say use the spec's 180 twentieths of a point (1/8 inch) instead of a fixed 0.5 cm; across and down can differ. Table Layout › View Gridlines (table.viewGridlines) is on by default, as in Word, so tables without borders still show their cells. Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
da18ae8933 |
File › Info: Title, Author and the other property fields keep what you type, and browser saves advance the save stamps (#260, #262) (#268)
The Info page edited a copy of the document properties that was re-read from the document every frame and only written back when a field lost focus, so each keystroke vanished on the next frame and nothing was ever committed. The fields now commit through file.properties as you type (undoable, one step per visit to a field, marks the document dirty). The web build's Save writes the bytes itself instead of running file.save, so it never stamped the modified time, last-modified-by and revision. Both paths now share Session::stamp_save. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
15f5ad274e |
Column Selection: Alt+drag selects a block of text (#237)
select.column (Mod+Shift+F8) selects the same x range on a run of lines, from two positions, two page points (Alt+drag), or by toggling column mode where the arrow keys extend the block. Copy/Cut give one line per row, Delete/Backspace remove only the block, character formatting and Change Case apply per row, typing/paste replace the block. Any ordinary caret move drops it. F8 keys now reach their shortcuts in the UI. 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> |
||
|
|
e71c2b6c5b |
Style Inspector: styles.inspector pane, styles.inspect query and per-level clearing (#236)
styles.inspector shows/hides the Style Inspector pane and returns the paragraph style, character style and the direct formatting that differs from each; styles.inspect is the pure query; styles.inspectorClear resets one level (paragraphStyle, paragraphFormatting, characterStyle, characterFormatting) as a single undo step. The pane opens from the Styles pane, with a clear button per level. Parity floor raised to 87%. 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> |
||
|
|
9c3ad84d33 |
Advanced Find: every match, Reading Highlight, search within a selection (#234)
Home › Editing › Find › Advanced Find (edit.advancedFind) opens the Find
and Replace dialog. With params it returns every match in the body, or
only inside the selection ("in": "selection"), and "highlight": true
turns on Reading Highlight, which marks all matches on the page and
follows edits until it's turned off. The ribbon's Find button becomes a
menu with Find, Advanced Find and Go To; the Find dialog gets a Reading
Highlight toggle. Parity 355/405.
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>
|
||
|
|
19e2e37ca6 |
View › Gridlines shows the drawing grid; Table Layout › View Gridlines outlines cells (#69) (#243)
View › Show › Gridlines only outlined table cells, so with no table on the page it seemed to do nothing. It now draws a light 0.5 cm grid over the page's text area, inside the margins, in Print Layout. Table Layout › View Gridlines gets its own command, table.viewGridlines, with its own view flag, and draws the table cell outlines as before. The ribbon button, keytip and parity catalog point at it. Both are screen-only overlays drawn by the canvas; print, PDF and image exports never show them. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
a9eb233a9d |
Track changes: keep other authors' deletions, credit paragraph splits, exact edit snapshots (split from #54) (#125)
* Track changes: a tracked delete keeps other authors' deletions Deleting a range with Track Changes on marked every run in it as deleted by the current author, also text another author had already deleted. That author's deletion was lost: the change list showed it under the wrong name, and rejecting the second author's changes brought back text the first author had deleted. Text that is already deleted now keeps its deletion and its author, like Word. * Track changes: a tracked paragraph split credits the new mark to its author Pressing Enter with Track Changes on inserts a paragraph mark by the current author, but the paragraph after the split took its mark's revisions from the text at the caret. Inside or at the end of another author's insertion, the original paragraph mark then showed as inserted by that author. The paragraph after the split now keeps the original mark's revisions; only the new mark is the splitting author's. * Engine: edit snapshots that take commands back exactly Session::edit_snapshot and Session::restore let a caller run a command and take it back without a trace: the document, the selection, the undo and redo stacks, the open typing group, a pending join_next_undo and the dirty flag come back as they were, and the layout is recomputed. Edit > Undo cannot do this: it leaves a redo step, loses the redo stack the command cleared, and closes the typing group. At the undo limit (500 steps) a command pushes the oldest step out. The snapshot keeps the oldest steps when the stack is near the limit and restore puts them back, so the stack is exact there too (up to four commands between snapshot and restore). Restore is exact for commands that add undo steps; edit.undo and file.new/file.open in between are outside its contract (documented). The snapshot destructures every Session field, so a new field does not build until someone decides whether a snapshot must cover it. Session::undo_depth and the snapshot's doc/dirty/undo_depth accessors tell a caller whether the command changed anything. * Tests: hostile_params_never_panic leaves out review.readAloud Read Aloud hands the document's text to the system's speech synthesizer (say, PowerShell, spd-say or espeak), so running the test made the computer speak the sample document. Like the file commands, it is left out of the sweep. * Undo: evictions at the limit are counted everywhere edit snapshots rely on them Merging main brought two paths #125's eviction counter didn't know: push_undo (typing parity, #204) dropped the oldest step without counting it, and a failed command (#153) put the whole stacks back but left the count raised. Either made restore() bring back a step that had never left. Both now keep the count right; restore_is_exact_at_the_undo_limit covers each (both fail without the fix). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: lpgn <2556951+lpgn@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
10eb8798cd |
Equations: Word (OMML) import, layout and save, plus an equation editor like Word's (#191)
* M5.5: Word equations (OMML) import, lay out and save like Word Equations were flattened to plain text on import. Now OMML is read into a math tree (fractions, scripts, radicals, n-ary operators, delimiters, functions, limits, accents, bars, boxes, equation arrays, matrices, phantoms), laid out with the OpenType MATH table (Cambria Math when installed, else STIX Two Math and other open math fonts), drawn to screen and PDF, and written back as the original OMML. Display equations sit on their own centred line. insert.equation parses the linear format into structure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M5.5: equation editor like Word's — Equation tab, in-place editing, # numbers Insert › Equation now opens an equation for in-place editing, with the Equation tab: ready-made equations, Unicode/LaTeX input and conversion (professional ↔ linear), Normal Text, eight symbol sets, and the eleven structure galleries (fraction, script, radical, integral, large operator, bracket, function, accent, limit and log, operator, matrix) with tiles rendered by the math engine. Typing builds up like Word (a/b, x^2, √, ∑… then space), Math AutoCorrect turns \alpha, \sqrt… into symbols, Tab moves between empty slots, arrows/Backspace work through structures, and the right-click menu has structure commands (fraction type, radical degree, limit position, matrix rows/columns, remove brackets…). A trailing #(n) numbers a display equation with the number at the right margin; # alone numbers it automatically. DOCX output keeps numbers Word-compatible. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * i18n: equation editor strings in Spanish, Brazilian Portuguese and Ukrainian The catalogs that landed on main after #191 was opened lacked the Equation tab and equation command labels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Equations: our own names and order for the built-in equations Clean room: the ready-made equation list used another product's gallery entry names and order. Same formulas (and ids), with our own descriptive labels, grouped by subject (geometry, algebra, calculus, trigonometry, probability). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ROADMAP: equations row matches the landed equation editor Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fmt after 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> |
||
|
|
e4456efb84 |
Text boxes and floating objects: editing, drag/resize, wrapping, overflow, footnotes (#46)
* fix text boxes: type into a new box, click in and out, Escape leaves it Insert › Text Box left the caret in the body after the box and clicks inside a box never reached its story, so its text could not be edited. - insert.textBox puts the caret in the new box (when anchored in a top-level body paragraph, the only place layout shows box text today). - Layout emits a hit-test-only Placed::TextBox area; story_at prefers a box under the point (body text wins over boxes behind it) and ignores box lines outside their box. - Canvas: click enters a box, clicking outside goes back to the body (not while editing a header/footer); double-click in a box selects a word there. - Escape in a box puts the caret just after it (Document::text_box_anchor). - Editing a box doesn't dim the body or show "Editing header/footer". - Tests: engine (caret in box, typing, undo, table cell unchanged), layout (story_at inside/outside/behind), doc (anchor lookup). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix pasted text boxes sharing the original's text A copied fragment only carried the text box's part id, so a pasted box showed the original's story: editing either changed both, edits made after copying leaked into the paste, and pasting into another document (or Insert › Text from File) pointed at whatever part had that id there, e.g. a header. - Fragment carries copies of the text box stories it shows (nested boxes too). - insert_fragment gives every pasted box a new story of its own; a box whose story can't be found keeps none rather than showing an unrelated part. Nesting is depth-limited and one paste creates at most 4096 stories. - Insert › Text from File uses Document::body_fragment, which brings the file's text boxes along. - Tests: doc (own story, copy-time snapshot, other document with clashing ids, box in a table, fragment without parts, self-nested box), engine (copy/paste/type/undo through commands). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * word count includes text boxes, footnotes and endnotes (Word's default) The status bar and Word Count dialog counted only the body, so words typed into text boxes (and notes) were missing. - Document::counted_stories / word_count_including_notes: the body plus the text boxes, footnotes and endnotes it actually shows (nested too); orphaned stories (deleted or cut boxes), headers, footers and comments don't count. - Session::count_notes (default on) with Session::word_count for the status bar; review.wordCount takes {"includeTextBoxes"?: bool} and reports it. - Word Count dialog: "Include text boxes, footnotes and endnotes" checkbox. - Document::word_count stays body-only (docx Words property, NUMWORDS, stats). - Tests: doc (used vs orphaned stories, nesting, self-nested box), engine (default on, toggle sticks, deleting a box drops its words). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * canvas: select, drag to move and resize pictures, shapes and text boxes Click a picture or shape (or a text box's border) to select it: a frame with eight handles. Drag it to move it, drag a handle to resize it (pictures keep their proportions from a corner, Shift toggles), arrows nudge a floating one (Ctrl/Alt: 1 pt), Escape cancels a drag. While a text box's text is edited its frame shows dashed with handles. Typing or Enter with a text box selected goes into it. Reusable pieces, no per-kind code: - layout: Placed::Object, one hit area for every picture/shape/text box (replaces the text-box-only item); DocLayout::object_at / object / text_box / find_object (page-hinted, no whole-document scans per frame). - engine: arrange.bounds {width,height,x,y,page} (floating objects keep their anchors on the same page; inline ones float where dropped; another page re-anchors to the nearest line) and arrange.nudge; objects::min_size shared with the UI. - ui: frame.rs, a document-agnostic transform frame (handles, hit testing, drag/resize maths, painting); objects.rs wires it to document objects. - geom: Rect::expand. control: ui.press / ui.release for half-way drags. Rendering: nothing is laid out or rasterised while dragging. The preview is the object's own pixels from the cached page texture (moves, picture resizes) or a light outline (text box/shape resizes); the drop is one command, one relayout, one undo step. Tests: frame maths, layout hit areas, engine move/resize/nudge across anchors and pages, hostile values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * moving an object anchors it to the paragraph it's dropped on, so text wraps A floating object only pushes aside text from its anchor paragraph down, and a move kept the old anchor: drag a text box above its paragraph and the text there ran straight under it. - arrange.bounds now anchors the object to the paragraph under its top edge (the last one starting at or above it on that page) and positions it relative to that paragraph's column and top, as Word does: the text it lands on wraps, and it moves with that text. On a page where no paragraph starts it anchors to the nearest line, positioned on the page. - layout: Placed::Object / ObjectHit carry the paragraph origin that column- and paragraph-relative offsets start from. - engine: edit_obj / edit_float change an object at a position (with_obj uses it). - Test: a box dropped over an earlier paragraph anchors there and the lines beside it stop short of it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * text boxes hide text that doesn't fit, as in Word Text past a text box's bottom spilled out over the page below it. - layout: fit_box keeps what fits in a box: whole lines that fit (a line that only partly fits is hidden too) and other items starting inside; the first line always stays so even a tiny box shows something. Done in layout, so the canvas, PNG and PDF agree and hidden text can't be clicked; no renderer changes. Reusable for anything laid out into a fixed height. - BOX_INSET_X / BOX_INSET_Y name the text box internal margins (were 7.2/3.6 literals). - Test: overflow hidden, every shown line inside the box, a tall box shows everything, a minimum-size box shows its first line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * square wrap flows text on both sides of a floating object Text only used the wider side of a floating picture, shape or text box; Word's Square wrap fills both sides when there's room. - para: row_spans gives the spans of a row around the objects in its way (each at least 0.5" wide, else the line goes below); a row cut in two is two Lines sharing top and height, the second marked `beside`. Everything per line (alignment and justification per side, hit testing, caret, selection, drawing, PDF) works unchanged; a row is as tall as its tallest part. - layout: a row stays on one page; line numbers count rows, not parts. - Exclusions that don't overlap a line horizontally no longer narrow it. - Test: text both sides of a mid-column box (same row, left then right, hittable), a too-narrow gap stays empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * drop a deleted or cut text box's text from the document Deleting or cutting a text box left its story in Document::parts: never shown or saved to .docx, but listed by document.inspect and kept in memory. - doc: prune_text_boxes removes the text box stories no box shows (nested ones too); free without text boxes, one paragraph pass otherwise. Session runs it after every editing command. Undo snapshots and the clipboard hold their own copies, so undo and cut/paste are unaffected. - doc: reachable() walks the stories objects lead to; counted_stories (word count) and prune_text_boxes share it, and it walks blocks directly instead of building paragraph paths. - Tests: orphaned and nested-orphan boxes go, shown ones (body, nested, in a header) stay; delete then undo, cut then paste. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * word count's include-text-boxes setting survives a restart The Word Count dialog's "Include text boxes, footnotes and endnotes" was a session field, so it reset to on every launch. - engine: Prefs, the editing preferences the front end saves between runs (count_notes moves there; Session::prefs is the one source of truth). - ui: SavedPrefs pairs UiState with the engine Prefs; WordApp::saved_prefs / restore_prefs. UI fields stay at the top level of ui.json, so existing files still load (the new `editing` section defaults). - wordcraft app: saves and restores SavedPrefs instead of UiState alone. - Tests: an old prefs file loads with Word's default; the setting round-trips. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * text boxes and floating objects outside the body; footnotes in text boxes Text boxes in table cells, headers, footers, footnotes and other text boxes showed an empty frame (or nothing), and floating pictures and shapes there weren't drawn. Footnotes inserted inside a text box were never numbered or placed. - layout: place_objects, one routine for the pictures, shapes and text boxes in a paragraph's lines (floating ones drawn, every one's area, text boxes' text), used by body paragraphs and by layout_box (cells, headers, footers, notes, box contents). Floats are positioned against an AnchorFrame (column, paragraph, margins, page); headers and footers pass where the page is, so page/margin-anchored objects land right (a footer is laid out again only when it has floating objects); elsewhere they're placed in their container. Nested boxes show MAX_TEXT_BOX_DEPTH (doc) levels deep. - Placed::Object / ObjectHit carry their story; DocLayout::object(&Pos), object lookups include headers and footers, header_footer_text_box_at. A part's lines are looked up in headers and footers too (caret, typing). - Footnotes: numbered in reading order with text boxes where they are (Document::objects_in_reading_order); a box's footnotes go on the page of its line (Document::notes_in_text_box), sharing the body's reservation. - engine: insert.textBox always puts the caret in the new box; disabled inside a text box (as in Word). - canvas: click into a header/footer's text boxes while editing it; leaving a box returns to where it is; body dimming computed once per frame and used for both the render and its cache key. objects: only body objects are selectable, only body paragraphs' objects movable; frames off the body show without handles. - Tests: boxes in header (inline and page-placed), footer, cell and note show text and take the caret; footnotes in a box numbered and placed in order; a box in a table cell takes the caret and its text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * word count: a selected object counts the whole document, in the dialog too With a picture, shape or text box selected the status bar showed the document's total but Review > Word Count counted the selection (0 words). - engine: cmd::objects::object_selection, "the selection is exactly one object"; review.wordCount counts the document then. The UI's objects::selected now uses it (one definition). - Test: an object selection counts the document; a text selection itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * review: bound nested text box expansion; tidy-ups Blocker: laying out a text box lays out the boxes inside it, which could fan out unboundedly (a box whose shapes show itself, or chains of boxes each showing the next many times) - 30 shapes made 1.7M layout items. Footnote numbering's walk fanned out the same way. - doc: BoxBudget, one bound for both: a box isn't expanded inside itself, nesting stops at MAX_TEXT_BOX_DEPTH, an outermost box expands at most 64 boxes inside it (so one hostile box can't starve the next page's header boxes) and a pass at most 20,000. - layout: Ctx carries a BoxBudget for the whole layout; place_objects asks it before laying out a box's text. DocLayout::text_boxes reports how many were. - doc: walk_objects (objects_in_reading_order, notes_in_text_box) uses it too. - Tests (crafted documents): a box showing itself 30 times lays out once; a five-level chain fanning out 30 ways stays within the budget, for layout and for the walk; a box inside a box still shows its text. Weakening the budget makes them fail (20,000 boxes / 600,001 visits). Tidy-ups: - para: row spans in a VecDeque (pop_front, not remove(0)). - fit_box: a text box's text area excludes both the top and bottom margins. - theme: Tokens::handle / handle_shadow for the frame handles (light, dark); the drag preview's opacity is a named constant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * box budget: no overall cap; bound the .docx writer too; tests An audit of the previous commit found: - The 20,000-box overall cap starved ordinary long documents: one budget per layout, spent by body boxes first, then one per page for header boxes (two for footers with floats), so a long document's header boxes went blank on every page. It wasn't needed: only nesting grows exponentially, and MAX_NESTED caps that per outermost box, keeping the work linear. Removed. - The .docx writer still expanded a shape's story only depth-limited (a box showing itself 30 times saved as ~28,000 nested copies, ~7 MB zipped). It now uses the same BoxBudget, and only for text box parts. - Layout expanded a shape's story whatever its part kind (a shape pointing at a header showed the header's text); now text box parts only, as the walk does. The story's blocks are cloned only once the budget agrees. - Translations of "Count words in text boxes and notes" said footnotes only; now footnotes and endnotes, in each catalog's own terms. - Tests: no timing assert or leftovers; a long document with 400 boxes and a header box shows all of them on every page; notes_in_text_box follows nested boxes once; saving a self-showing box stays small (fails at 7 MB without the bound). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Arrange: nudging or dragging an aligned float keeps it in place first main (#136) reads w:align into Float.h_align/v_align, which overrides the offset; the PR's nudge only changed the offset and drag only reset the anchors. Nudge now converts an aligned axis into a column/paragraph offset that keeps the object where it is, and a drag clears the alignment. Test included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fonts: a lookup that races another thread's load of the same family no longer falls back FontDb::face fell back to Source Sans 3 when its load_cataloged added nothing, which happens when another thread loaded the family between the miss and the load. word::resolve memoises that, so the wrong font stuck for the process; it made layout tests on main flaky (justified_lines_shrink_spaces_in_word_2013_mode, space_shrinking_is_only_for_justified_text failed 6 of 15 runs on origin/main). Look again after loading whatever it returns. Regression test races 8 threads on fresh databases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layout tests: text box and two-sided wrap tests measure what they mean square_wrap_flows_text_on_both_sides compared xs.last() (the end of a trailing space, which may hang past the edge) with the edge; compare the last non-space cluster's right edge. text_box_hides_text_that_does_not_fit compared a tall box's lines with another document's line count; compare with its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Weeb Labs <weeblabs@VFIEVOX3.Router> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com> |
||
|
|
fe7e79ad70 |
Read Aloud: player with speed control, sentence skipping, skips citations (#190)
* Zotero 1: range fields and custom document properties round-trip Citation managers' ADDIN fields (Zotero, Mendeley, EndNote) are now kept as range fields: FieldStart/FieldEnd markers with the result as ordinary, formatted content between them, possibly spanning paragraphs (a Zotero bibliography). Before, the result was flattened to plain text and a multi-paragraph field lost its end. - doc: InlineObject::FieldStart/FieldEnd, Document::field_ranges, balance_field_ranges; CustomProp and custom_props with get/set/remove. - docx: read ADDIN fields as ranges (field codes up to 2 MiB), write them back as begin/instr/separate…end; orphan markers are dropped on save so files always hold whole fields. docProps/custom.xml is parsed and regenerated (simple types as text, vectors/blobs verbatim) instead of passed through, so Zotero's ZOTERO_PREF_n settings are editable. - engine: file.properties reads and sets custom properties. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Zotero 2: wire-protocol client and document bridge New crate wordcraft-zotero (L5) speaks Zotero's published word-processor protocol (port 23116; 32-bit transaction id + big-endian length frames, JSON payloads, ERR: replies): - client: sends an integration command (addEditCitation, addEditBibliography, addNote, refresh, removeCodes, setDocPrefs) and answers Zotero's calls until Document_complete or the connection closes (cancelled dialog). Answers go through a callback so the app can apply them on its UI thread; run_on_session answers directly. - Bridge: implements Zotero's Document_*/Field_* calls on a Session. Fields are stored as Word's plugin stores them (ADDIN ZOTERO_ range fields; prefs in ZOTERO_PREF_n, with Zotero's ReferenceMark field type kept as Word's Field in the file), so documents stay Word-compatible. Rich text (RTF or HTML) keeps citation formatting; bibliographies get a Bibliography style from setBibliographyStyle; note styles create footnotes/endnotes, and deleting a lone note citation removes its note; placeholder links convert to fields. One transaction = one undo step. Field ids stay stable through inserts and deletes. - Tests: a fake Zotero over a real socket, every call, docx round trip, hostile arguments for every method, random frames (proptest). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Zotero 3: Zotero in the app and the CLI, tested with the real Zotero - ui.zotero.<command> (addEditCitation, addEditBibliography, addNote, refresh, removeCodes, setDocPrefs), ui.zotero.status and ui.zotero.answer. The socket runs on a background thread; each of Zotero's calls is answered on the UI thread against the session, and Zotero's questions become a dialog the session waits on. Web builds report that Zotero needs the desktop app. - wordcraft-cli zotero <command> <file> [--save] [--trace] [--port]. - Bridge: a selection Zotero makes to show a citation is put back when the session ends. - ui_shot: {"wait": ms} keeps frames running for background work. - docs/zotero.md. Checked against Zotero 10.0.6: refresh of a citation and bibliography (APA), its "modified citation" question answered in the app's dialog, RTF results with italics, Bibliography style, prefs round trip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Zotero 4: switching between in-text and note styles moves citations Document_convert and Field_convert now move a citation between the text, footnotes and endnotes, as Zotero asks when the document's style changes between an in-text and a note style: into a new note at the same place, or back into the text where the note mark was (removing a note that held only the citation; keeping one with other text). Field ids survive the move, formatting is kept, and each Zotero session stays one undo step. Checked against Zotero 10.0.6: unlink citations (removeCodes, with its confirmation question) and a refresh after switching the document to Chicago notes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Zotero 5: Zotero tab in the ribbon A Zotero tab (before Help) with Add/Edit Citation, Add Note, Add/Edit Bibliography, Refresh, Document Preferences and Unlink Citations. The buttons are disabled while a Zotero command is running and in the web build. Three new icons drawn in code (document preferences, unlink, add note); the others reuse the References tab's. Labels and status messages translated (zh-hans, zh-hant, ja). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Zotero: Add/Edit Bibliography inserted nothing Zotero reads a newly inserted bibliography field back before filling it, and unlinks a field whose text is empty, taking it for a bibliography the user deleted. Our new fields were empty, so Add/Edit Bibliography ended with Field_removeCode and nothing in the document. New fields now hold "{Citation}" until Zotero writes the result. Found by tracing the session with Zotero 10.0.6; regression test added. wordcraft-cli zotero takes --cmd (run before the Zotero command, e.g. caret.docEnd) to reproduce sessions from a given caret position. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Zotero: log each command's outcome and Zotero's calls to the app log Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Read Aloud player: speed, previous/next sentence, skips citations Read Aloud now speaks sentence by sentence with the system speech program (say / espeak / Windows SAPI) on a worker thread, so it can pause, skip and change speed. A floating player window has previous / play-pause / next / stop, a 0.5-3x speed slider and "Skip citations & bibliography" (on by default: Zotero, Mendeley, EndNote and Word citation fields). The spoken sentence is highlighted and the caret follows it. New commands: readAloud.playPause/next/previous/stop/speed/ skipCitations/status, and caret.pastCitation (Zotero > Move Past Citation). Speed and skip preference persist with the UI prefs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cargo.lock: wordcraft-zotero at the workspace version (0.4.0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Read Aloud: no unused BASE_WPM warning on wasm Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Read Aloud: fuzz review.readAloud in hostile_params_never_panic #190 makes the engine's speech backend silent (Hold) under cfg(test), so the #75 skip is no longer needed; assert the backend instead so a test can never start the system synthesiser. 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> |
||
|
|
6c152055b9 |
Hidden text: no line breaks, hyphens, wrap areas or bookmark text from text that isn't shown (builds on #152) (#173)
* Tables: stop repeating a header row that leaves no room for the next row A repeating header row plus a row that could not fit below it on an empty page made place_table loop: after each page break the header was placed again, so the page was never "at top" when the row was retried, split_row had nothing to split, and the loop ran its 1,000 iterations per row. A 20-row table with a 640 pt header came out as 19,002 pages. After a page break the header now repeats only when the row (or its first split) still fits below it; otherwise the row starts at the top of the page, as Word does when a header row would fill the page. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layout: give runs with different links or decorations their own style Builder::style memoised StyleRuns by a key that folded strike, double strike and "has a link" into one boolean and left out the underline colour and baseline position. The display list reads all of these from the shared StyleRun, so in a paragraph of like-formatted links every link pointed at the first one's URL in the PDF (and an external link after an internal one became a bookmark jump), double strike drew as single strike after a struck run, and underline colours were copied. The key is now the whole resolved format, so runs share a StyleRun only when nothing the display list reads differs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * PDF and No Markup: leave tracked deletions out of the final text PDF export defaults to no markup, but it laid the document out with every deleted run in place and only switched off the strike and colour, so the PDF printed deleted text as ordinary text ("Keep DELETEDTEXT INSERTED"): neither the original nor the final document, and a leak of text the author removed. The No Markup view did the same on screen. A new LayoutOptions::hide_deleted lays deleted runs out like hidden text (zero-width markers, so offsets, carets and hit testing keep working). PDF export sets it whenever markup is off, the session layout sets it when markup is hidden, and the markup toggles relayout as Show/Hide marks does. The display list also never draws a deletion as plain text when markup is off, whatever layout it is given. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tables: repeat the header on every page and put the next row under it, as Word does The earlier fix for the runaway loop dropped the repeated header row on any page where the header and the next row did not fit together. Word never does that: measured on Word for Mac, it moves the row to a new page, repeats the header there and places the row under it even when the row then runs into the bottom margin (a 300 pt header with 400 pt rows gives one header + row per page; a 640 pt header with 100 pt rows does the same). place_table now remembers when the page holds only the repeated header rows. A row that cannot split is placed there anyway instead of breaking the page again, which is both Word's behaviour and what keeps the loop from spinning: every pass now places something. Rows of text taller than the room under the header still split across pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * PDF: keep tracked deletions out of bookmarks and heading tags Without markup the PDF pages show the final text, but bookmark titles and the titles of heading structure tags were still built from plain_text(), which includes deleted runs. A heading "Intro SECRET Part" with "SECRET " deleted printed as "Intro Part" yet bookmarked as "Intro SECRET Part", and a fully deleted heading still got a bookmark: text the author removed leaked into the outline and the tag tree. Paragraph::final_text() gives the text with deletions left out, and the exporter titles headings with it unless markup is included. A heading whose text is all deleted has an empty title and so no bookmark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * No Markup: deleted note references take no number and print no endnote With deletions hidden, a tracked-deleted footnote or endnote reference disappears from the text, but note_numbers() still counted it. Deleting the first of two footnote references left the surviving one numbered 2, and a deleted endnote reference still printed its endnote after the last paragraph, because the endnote list is taken from the same map. note_numbers() now skips references inside tracked deletions when the layout hides them, so the remaining notes number from 1 and endnotes whose reference was deleted are not emitted. With markup shown nothing changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * No Markup: a deleted floating object leaves no wrap area behind With deletions hidden, a tracked-deleted floating picture or shape was not drawn, but place_para had already added its wrap exclusion from p.objects without looking at the anchor, so the final text still flowed around an invisible object (indented beside a square-wrapped shape). Floating objects whose anchor is a tracked deletion are now skipped when the layout hides deletions; deleted_objects() is shared with note numbering, which needs the same test. With markup shown nothing changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * No Markup: a deleted soft hyphen no longer breaks the word with a hyphen With deletions hidden, a tracked-deleted soft hyphen became a zero-width marker, but hyphenation_points() reads the paragraph's raw text and accepted marker clusters, so the line could still break there and draw "ab-" in a narrow column although the final text has no soft hyphen. When the layout hides deletions, clusters inside a tracked deletion are no longer hyphenation points (soft hyphens or dictionary points alike). With markup shown nothing changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * No Markup: tracked deletions add no line-break opportunities Break opportunities came from running UAX #14 over the paragraph's raw text, deleted runs included. With deletions hidden, a deleted soft hyphen or space still let the line break there: "xx ab<del>SHY</del>cdef" in a narrow column came out as "xx ab" / "cdef" instead of moving the word "abcdef" to the next line whole. When the layout hides deletions, the opportunities are computed over the text without them and mapped back to offsets in the paragraph. With markup shown, or with no deletions, the computation is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hidden text: no hyphenation points or line breaks in text that is not shown Hidden text that is not shown (the default, and always in print and PDF) is laid out as zero-width markers, but break opportunities came from UAX #14 over the paragraph's raw text and hyphenation_points() accepted marker clusters, so a hidden space or soft hyphen still let the line break there: "xx ab<hidden SHY>cdef" in a narrow column came out "xx ab-" / "cdef". Measured in Word for Mac, the hidden soft hyphen and the hidden space are no place to break: "xx" / "abcdef". left_out() is now the one test for text the layout leaves out (hidden text not shown, tracked deletions without markup). The run loop records the ranges it leaves out, and both the break opportunities and the hyphenation points come from that, so deletions behave exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hidden text: a floating object anchored in it leaves no wrap area behind With hidden text not shown, a floating picture or shape anchored in a hidden run was not drawn (the run is a zero-width marker, so no object cluster places it), but place_para had already added its wrap exclusion, so the text still flowed around an invisible object. Measured in Word for Mac: a square-wrapped picture anchored in hidden text is not printed and the text runs the full width. deleted_objects() becomes left_out_objects(), which resolves each anchor's formatting through the styles (hidden text often comes from a character style) and applies the same left_out() test as the paragraph layout. Note numbering keeps counting hidden reference marks, as Word does, so it asks only about deletions; its behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hidden text: print the footnote of a hidden reference mark, as Word does A footnote whose reference mark is hidden text was numbered but never printed: the hidden run became zero-width markers without recording the note, so place_para never put the footnote at the foot of the page. Measured in Word for Mac with hidden text not printed, a hidden footnote or endnote reference still takes its number (the next visible footnote is 2) and both notes are printed. Endnotes already were, and the numbering already matched. A hidden reference mark now records its note like a visible one, so the footnote goes to the foot of the page holding the line it sits on. A deleted reference in the final text still records nothing, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * PDF: keep hidden text out of bookmarks and heading tags The PDF never prints hidden text (its layout does not show it), but the bookmark and heading-tag titles were built from final_text(), which drops tracked deletions only. A heading "Intro SECRET Part" with "SECRET " hidden printed as "Intro Part" yet bookmarked as "Intro SECRET Part", and a heading that is all hidden text still got a bookmark for a heading nobody can see. Paragraph::text_where() generalises final_text() to any per-run test, and the exporter titles headings with the runs that are neither hidden (resolved through the styles, since a character style can hide text) nor, without markup, deleted. A heading that is all hidden has an empty title and so no bookmark. Word's PDF export from AppleScript writes no outline, so there is no Word answer to match; this follows what the pages show, as for deletions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * PDF: titles leave out what the layout left out, table formatting included Bookmark and heading-tag titles resolved hidden text from the paragraph and run formatting alone, but the layout also applies the table style's conditional formatting (header row, first column, total row) beneath the runs. A Heading 1 in a table header row whose style hides it printed nothing yet still got a bookmark, and a partly hidden heading leaked the hidden part into its tag. ParaLayout now records the byte ranges it laid out as nothing (left_out), resolved exactly as laid out, and the exporter titles a heading from the text outside them (plus tracked deletions without markup, as before), so the titles cannot disagree with the pages. text_where() gives way to text_without() over byte ranges; final_text() is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hyphenation: hyphenate words as laid out, without hidden or deleted text Automatic hyphenation looked up the words of the paragraph's raw text and only dropped points that fell inside text the layout leaves out. Words and the minimum-letters limits still came from the raw text: with "accep" hidden in "acceptable", the visible "table" could break as "t-able" (the point after "accept"), and a hidden space split "foot<space>ball" into two words, so the visible "football" never hyphenated. Soft hyphens and dictionary points are now found in the same text as laid out that the UAX #14 break opportunities already use, and mapped back to paragraph offsets. With hidden text shown and markup on, that text is the paragraph's own and nothing changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layout, PDF: skip left-out text in one pass, not a scan per char Building the text as laid out tested every char against every left-out range, and Paragraph::text_without did the same for PDF titles. Hidden text in many runs (formatting that alternates, a common result of imports) made both quadratic: a 100,000-char hidden heading in one-char runs took 13.9 s to convert with the release CLI, 400,000 chars minutes. The layout now merges adjacent left-out runs as it records them and walks the ordered ranges with a cursor; text_without sorts its ranges by start (they may come in any order and overlap) and walks them the same way. Both are linear: 0.20 s for that heading, 0.81 s at 400,000 chars, with the same output. No left-out text still means no filtering at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hyphenation: check the limits of each point in constant time hyphen_points() found each candidate point's letter run by scanning out from the point and counted the letters on each side by rescanning the run, and the syllable heuristic rebuilt the rest of the word as a string for every position. Both are quadratic in the word's length. Ordinary words are short, but with hidden separators the layout now hyphenates the visible word, so "hyphenation " repeated 8,192 times with every space hidden is one 90,112-letter word: 2.0 s to convert with the release CLI. Letter and uppercase counts before each char, the letter run of each char and the heuristic's next vowel and byte offsets are now computed once, so every point is checked in constant time: that document converts in 0.09 s. The output is unchanged: the old functions stay in the tests as the reference, compared over awkward words and every combination of limits, and a hyphenated document's PDF text is identical. No maximum word length is imposed: the work is linear, so a cap would only change output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layout: find the formatting of every object anchor in one walk left_out_objects() asked props_of_char() for each floating object's anchor, and props_of_char() scans the runs from the start, so a paragraph with many anchors in their own runs cost a scan per anchor, quadratic, and paid it on every layout because it runs before the paragraph cache is consulted: 40,000 hidden anchors took 0.69 s, against 0.05 s for 10,000. The runs and the object offsets are both in order, so one walk now finds each anchor's run, resolving a run's formatting once; past the last run it is the paragraph mark, as props_of_char() gives. With hidden text shown and markup on nothing can be left out and no work is done at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layout: find a run's style without formatting it as a string #152 keyed each run's style by its whole format's Debug string, built for every shaped piece; on a long document with many runs that doubled layout time (3000 paragraphs x 40 linked runs: 12.7 s vs 6.4 s for the old key). Key by a hash of the commonly varying fields and confirm by comparing formats: 3.8 s. A capped bucket scan keeps formats that never compare equal (NaN) linear. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Layout: floats hidden in table cells take no room, table style included Test the layout_box path the merge extended (a float anchored in hidden text in a table cell is not placed and leaves no wrap area), and resolve the anchors' formatting with the table style's character formatting under the runs' own, as the cell's text is laid out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: linear-time checks compare 8x the input against a 24x limit At 4x with an 8x limit a busy machine could push a linear layout over the line (8.8x seen once in a full workspace run); at 8x a linear layout takes about 8x and a quadratic one about 64x, so 24x stays far from both. 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> |
||
|
|
802ba30441 |
Desktop and web: ask to save changes before New, Open or Close; AutoSave only files you saved (#151)
* Desktop and web: ask to save changes before New, Open, Close or quit New (Mod+N, File › New), Open (once a file is picked, recent files, dropped files), Close (Mod+W) and the window's close button replaced or closed a document with unsaved changes without asking, and its undo history went with it: `Session::set_document` drops both, and nothing checked `dirty` first. Closing or reloading the browser tab lost the document the same way. `WordApp::run`, the path every user action takes, now stops New, Open with a path and Close on a dirty document and shows "Do you want to save changes to <name>?" with Save / Don't Save / Cancel; the command runs once it is answered (`ui.saveChanges`, so agents can answer it too). Save goes through the normal save, or Save As for a document without a file, and a failed or cancelled save cancels. The window's close button is held with `ViewportCommand::CancelClose` until the prompt is answered. Scripts and agents keep the old behaviour: the control channel (and the MCP bridge over it) now calls `WordApp::execute`, which never asks, as programmatic calls never open dialogs. While the prompt is up, keys go to it rather than to the page behind (Enter would otherwise type a paragraph mark into the document being saved). On the web, a `beforeunload` listener asks the browser to confirm leaving with unsaved changes, and Export (PDF/PNG) no longer marks the document saved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * AutoSave: only write to a file the user saved to, and report failures AutoSave was on for every document with a path, and `file.open` sets the path, so opening contract.docx and typing one character rewrote it in place about 2.5 s later. A save drops whatever the readers don't model (content controls, charts, SmartArt, macros…), so that content was deleted from the original without the user ever pressing Save, and undo can't bring it back. Save errors were discarded with `let _ =` and retried every 2.5 s. AutoSave now writes only to the file the document was explicitly saved to (Save / Save As) in this session; a file that was only opened is left alone until the user saves it. New and Open reset that. A failed AutoSave is shown in the status bar and logged, and turns AutoSave off for that file until the next explicit save instead of retrying forever. The title-bar toggle shows on only when AutoSave actually covers the document; clicking it on an unsaved or merely opened document starts Save As, as it already did for a new one. The Options label now says which documents AutoSave covers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Save Changes prompt: Enter clicks the focused button, not always Save With Cancel or Don't Save focused (Tab), egui turns Enter into a click on that button, but the dialog then also checked `key_pressed(Enter)` on its own and overwrote the answer with Save. Cancelling a Close from the keyboard therefore saved the file over itself and quit, and Don't Save saved too. The answer now comes from the button that was clicked; Enter means Save only when none of the three buttons has focus (the usual case, with the page behind still holding focus). Escape still cancels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Save Changes prompt: forget it when its document is replaced A prompt asked about document A kept its pending action after a script or agent (control channel, MCP) opened document B. Don't Save then ran the retained New or Open against B and threw away B's edits, under a question that named A. `Session` now has a `document_id` that changes only when `set_document` replaces the document (open, new, mail merge, recover), never on an edit. The prompt records the id it asked about. Any command that replaces the document drops a pending prompt, and an answer that arrives anyway (a click in the same frame, `ui.saveChanges` from an agent) is refused when the id no longer matches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * AutoSave: keep it on when the Open picker is cancelled `file.open` without a path only asks the UI for the file picker, and it returns Ok. The AutoSave bookkeeping cleared the saved-file permission on any successful `file.open`, so pressing Mod+O and cancelling turned AutoSave off for a document that never changed, until the next explicit save. The permission is now cleared when the document is actually replaced, which `Session::document_id` tells apart from an edit. That also covers the other commands that replace it (mail merge, recover), which the id-based check missed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Web: tell the leave-page guard about edits made while drawing The `beforeunload` guard reads a flag the web shell copied from `session.dirty` after `logic`. Typing and formatting happen later in the same frame, in `ui`, so after the first keystroke in a clean document the flag still said "saved" until the next frame's `logic`. Closing or reloading the tab in that window left without asking. The app now reports its unsaved state to the host after both passes (`Services::on_dirty`, called at the end of `logic` and of `ui`), and the web shell sets the guard's flag from that instead of copying it itself. That puts the timing in `ui-egui`, where a native test can cover it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Save Changes prompt: a save that drops content doesn't count as saved The prompt's Save accepted any `saved: true`. Save As to a format that doesn't keep the whole document (a .png of page 1, .txt, .md) reports that while leaving the document unsaved, so New, Open or Close went ahead and the document, with its undo history, was gone. Save now lets the command continue only when the document is clean afterwards, i.e. it went to a format `file.save` treats as the document's own file (docx, odt, rtf, json). Otherwise the command is cancelled, the document stays open, and the status bar says to save it as .docx. A document opened from a .txt file therefore needs Save As .docx (or Don't Save) before it can be replaced: the engine already treats a text save as a copy. The web shell's download path cleared the unsaved flag for any save, whatever the format, so a .png or .txt "save" also silenced the prompt and the leave-page guard. It now clears it only for those formats. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Web: a failed download is not a save The download service returned nothing, and the web shell caught and only logged a failure to start the download. Save therefore always cleared the unsaved flag and reported `downloaded`, so the Save Changes prompt went ahead with New or Open (replacing the document and its undo history) even when nothing was downloaded, and the leave-page guard stopped asking. `Services::download` now returns a Result and the web shell passes its error through. On an error, Save leaves the document unsaved, shows the failure in the status bar and returns it, and the prompt cancels the pending command. Limit: a browser doesn't tell the page what happens after the download starts (the user may cancel it or the file may be blocked), so a download that started is still treated as saved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * M-i18n: Save Changes prompt and AutoSave strings in pt-br, es and uk The renamed AutoSave option, its failure message, the Save Changes prompt and the failed-download message had rows only in ja/zh catalogs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Save Changes and AutoSave: .docm, .dotx and .dotm keep everything (#172) Since #172 these formats keep macros and content types, and file.save already treats them as the document's own file. keeps_everything() and AutoSave now share one list that includes them, so a web download of a .docm marks the document saved and AutoSave covers a .docm/.dotx/.dotm saved this session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Save As: keep main's suggested name (.docm stays .docm) after the merge The merge kept the PR's save_as_dialog body, which reverted main's default_save_name() suggestion back to <title>.docx. 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> |
||
|
|
c0ccda288c |
Find/Replace skips text deleted under Track Changes (#89); a failed command keeps undo history (#153)
* Find/Replace: skip text deleted under Track Changes With Track Changes on, Replace marks the match deleted and leaves it in the paragraph text, then inserts the replacement in front of it. The search helper matched the raw paragraph text, so the next step found that same deleted "cat" right after the caret and replaced it again: four Replaces on "one cat two cat" gave "one dogdogdogdog two cat" and `remaining` never dropped. `search` (used by Find, Find Next/Previous, Replace, its `remaining` count and Replace All) now skips any match that overlaps a run marked deleted, so deleted text is never found, as in Word. A match that would run across deleted text is skipped too: that string is not in the document as it now reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Undo: a failed command keeps the redo stack and the oldest undo step `run` checkpoints a mutating command before running it, and the checkpoint clears the redo stack (and, once the history is at its limit, drops the oldest step). When the command then failed, the error path put back the document, selection and typing group and truncated the history, but the cleared redo stack stayed cleared. Insert, Undo, then any command that fails on its parameters (`text.insert {}`, `para.align {value:"bogus"}`) made the undone work unrecoverable. At the limit, truncating to the old length also kept the failed command's empty step and lost the oldest real one. The checkpoint now hands back what it removed, and the error path restores both. join_next is deliberately left consumed: the drag that set it re-arms it every frame, and restoring it would merge the next unrelated command into the drag's undo step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Find/Replace: find live text next to a tracked deletion The previous fix rejected any regex match that overlapped tracked-deleted text, but `find_iter` had already consumed those positions, so a valid match overlapping the rejected one was never tried. "aaa" with the first "a" deleted reads "aa", yet Find "aa" found nothing (raw 0..2 was rejected and the scan resumed at 2) and Replace All replaced nothing. Searching the raw text also let deleted characters decide word boundaries: deleting the space in "cat fish" still matched whole-word "fish". `search` now builds each paragraph as it reads, without deleted runs, and searches each stretch of live text separately with `regex_automata::Input::range`. A match stays within one stretch, as before, and `\b`, `^` and `$` see the neighbouring live text, so a deletion neither creates nor hides a word boundary. `regex::Regex` cannot bound a search's end without slicing the haystack, which turns the slice end into a word boundary; regex-automata is the engine `regex` already uses (0.4.18, already in the lockfile), so no new code is built. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Undo: a failed command restores history changed by nested commands The previous fix had the checkpoint report what it evicted and put that back on failure, but it only saw the outer command's own checkpoint. Commands run nested commands, and a nested one that succeeds checkpoints too. `file.inspect {"remove":["comments","revisions"]}` in a comments-only document runs `review.deleteComment` (allowed) and then `review.acceptAll` (refused). With 499 undo steps, the nested checkpoint evicted the oldest step and the rollback, truncating to 499, kept the outer command's empty step instead. At 500, only one of the two evictions came back. Nested undo, redo and version restore change the stacks in ways a length cannot describe either. Undo steps are now `Arc<Undo>`, and `run` snapshots both stacks whole before a mutating command and puts them back on failure, so the history is exact whatever ran inside. The snapshot copies one pointer per step, under 1,000 refcount bumps and an 8 KB allocation at MAX_UNDO, beside the two document clones every mutating command already makes. Undo and Redo are not mutating commands, so a top-level Undo still moves its document out of the step without cloning it. Only an undo nested inside a mutating command (a macro) clones, because the snapshot still holds the step. This replaces the eviction-return plumbing, so `checkpoint` is back to its original form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: compare undo timelines without comment timestamps failed_command_restores_history_changed_by_nested_commands builds the same session twice and compares their documents. Comments are stamped with the time to the second, so when the clock crossed a second between the two builds the documents differed and the test failed (about 1 run in 12). Clear the stamp in the compared copies; the test still checks every other part of each document in the timeline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Undo: wrap the typing-parity undo step in Arc after merging main 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> |
||
|
|
869ac16110 |
Typing parity with Word: lists, AutoFormat and AutoCorrect as you type (#204)
* Typing parity with Word: lists, AutoFormat and AutoCorrect as you type
Typed side by side with Word (same keystrokes, results read back) and matched it:
- Enter after a list no longer gets stuck: the paragraph that leaves a list goes back to
Normal, and later Enters make blank paragraphs (the `num: 0` marker counted as a list item,
so every Enter re-ran "leave the list" and did nothing; the list indent also stuck to every
following paragraph).
- Enter on an empty nested item moves it up a level before leaving the list.
- Second Backspace at the start of an un-numbered list paragraph returns it to Normal
instead of joining it to the paragraph above.
- "1. " always starts numbering at 1; "a. " and "I. " start lettered/roman lists; "- " makes
a dash bullet.
- Undo right after an AutoFormat/AutoCorrect change undoes only that change.
- AutoCorrect: sentence capitals (with abbreviation/initial exceptions), runs on Enter too,
"word -- word" en dash, "wait..." ellipsis, 1/2 1/4 3/4, ordinal superscripts, web
addresses become links.
- Border lines: Enter after ---, ===, ***, ___, ~~~, ### draws a bottom border.
- Layout: no line break right after a slash ("and/or", URLs), as in Word.
- ui_shot: UI_SHOT_FULL=1 prints whole replies for scripted checks.
Regression tests in crates/engine/src/tests_typing.rs and the layout tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Typing parity: record Word's observed typing rules, two more tests
docs/typing-parity.md lists what Word does for each key sequence (lists, Backspace, Tab,
AutoCorrect, AutoFormat, slash line breaks) with the test that pins it, the known gaps, and how
to compare without taking over the shared machine. CLAUDE.md/AGENTS.md point at it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
61e2152250 | Ruler: dragging an indent marker is one undo step (#57) (#62) | ||
|
|
d95e09ba25 |
Clippy/fmt clean workspace, ROADMAP with honest parity estimate; xtask ci green
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
72262a2ea9 |
M3/M6/M12: PDF export (krilla), TXT/MD/HTML/RTF/ODT import+export, citations & bibliography, index, table of figures, cross-references, tools (repeat, macros, AutoCorrect, compare, accessibility, inspector, protection, versions, Quick Parts)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c29182e8bc |
M1: DOCX read/write (OOXML) wired into open/save; floating objects; picture & shape tools; mail merge, envelopes, labels; contextual spacing; export without proofing marks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d1a0ed39c0 |
M7.1: proofing — spelling with suggestions, grammar checks, squiggles, F7, right-click menu, thesaurus; web app; hyphenation dictionary
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b8dd46ccb5 |
M0.7: engine — session, undo, 200+ commands, Word feature catalog, samples
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |