Without a creation date in the document, each PDF is stamped with the
current time, so the expected PDF and the submitted one differed whenever a
clock second ticked over between them (seen on the macOS runner). The test
document now has a fixed creation date, and the test checks that both PDFs
carry it.
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* --help and -h print the usage for the app and every CLI command (#968)
wordcraft --help / -h printed nothing useful: the app tried to open a
file named --help and started. wordcraft-cli <command> --help failed with
"unknown option --help" for every command, and -h was taken as a file
name or ignored.
The app now prints its options and returns before the window opens.
The CLI checks for --help / -h after any known command before options
are validated or files and folders opened, and prints the shared usage.
Tests run both binaries for each command and flag; the app's test runs
without a display, so a regression fails instead of opening a window.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #968: test bare --help and -h on wordcraft-cli
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
#1527's engine test needs a system Turkish Hunspell dictionary; on
machines without one it failed the workspace tests.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The traffic lights grew in macOS 26, so the zoom button overlapped
AutoSave. Measure the zoom button through AppKit at start-up and size the
bar and its left inset from it, keeping the old layout as the minimum and
the fallback.
Claude-Session: https://claude.ai/code/session_01JTqaRSzgjKv75TzSC55HEY
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ODT: every frame inflated its picture entry again, and the byte budget
was checked only after inflating, so frames pointing at an entry that
didn't fit (or at any entry once the budget was used up) inflated it
again each time. Each entry is now inflated at most once, and only when
the size it declares fits in what is left; the bytes inflated and every
later copy are charged, so an entry that inflates past its declared
size uses the budget up and nothing is inflated after it.
.doc: inline pictures remembered only successes, so a picture that
couldn't be read or didn't fit was located, decoded and copied again
for every anchor on it, and an inline shape's BLIP was decoded against
the whole media cap rather than what was left. Failures are remembered
too, bytes are copied and decoded only within what is left of the cap,
and nothing is read once it is used up.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* proof: Turkish spelling, suggestions, grammar and hyphenation
* Turkish: dictionary tests skip when no Turkish Hunspell dictionary is installed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(pdf): inline pictures keep their alt text in tagged PDF (#1502)
Inline pictures were tagged as Figure with no alt, so the exporter fell
back to "picture" and screen readers announced every inline image that
way, while floating pictures kept their alt text.
Draw::Image now carries the picture's alt text from the InlineObject, and
the Placed::Lines branch of the PDF exporter passes it to the Figure tag,
including for rotated inline pictures. A picture without alt text still
falls back to "picture".
Adds a regression test in crates/pdf/src/tests.rs.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* rustfmt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* ODT: read and write tracked changes (gap 19)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
# ROADMAP.md
# crates/engine/src/tests.rs
# crates/formats/src/model.rs
# crates/formats/src/odt.rs
# docs/file-format-parity.md
# docs/gaps.md
* ODT tracked changes: deleted paragraph marks don't take the body's insertion; one region per deletion
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The bundled Moby list already knows many British forms (judgement,
defence, programme) but still flagged others: honour and its inflections,
colourful, colourless, rigour, glamour, prioritise, privatise,
synthesise, paediatric and a few more.
Adds those base forms to assets/spelling/en-extra.txt (inflections come
from the existing rules) and a regression test. A separate en-GB
dictionary chosen from w:lang is still open on the issue.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Lift equation color from run-level OMML to CharProps during read,
push it back to runs during write. Keep source OMML until the color
changes to preserve control properties the model can't hold.
* feat-ink-to-shape-arrows: WIP
* Draw: Ink to Shape recognises arrows (gap 12)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Ink to Shape arrows: hand-drawn shafts, more tests, docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Ink to Shape arrows: the tip is the first near-farthest point
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Ink to Shape arrows: step to the real tip on dense strokes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #1445: Save in the close prompt that can't write the file says so instead of looking like Cancel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1445: a failed save from the close prompt brings the prompt back with the reason, Save As included
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1315: web Save before New/Open waits for the save picker; cancel or failure keeps the document
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1315: overlapping saves hold the replacement; cancel and waiting messages
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- Table autofit measured every cell paragraph at an unbounded width through
the paragraph cache, so a long table held two layouts per cell. The cache
now keeps only the measured narrowest/widest widths (`LayoutCache::measures`,
same key and eviction as the layouts).
- The cache key formatted each cell's table-style text formatting with Debug
for every cell paragraph on every pass; the hash is now memoised per pass.
- Update Fields added the hidden ListEntry style even without a generated
list, which changed the layout environment and dropped the whole cache.
The style is now added only when a list is rebuilt.
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* #1252: dim highlights on a dark page so the text on them reads
On a dark page (View > Switch Modes) highlight and character-shading fills
were lightness-inverted like every other colour, which leaves saturated
hues such as yellow bright under white text. They are now marked as text
backgrounds (Draw::Fill text_bg) and dimmed toward black keeping their hue
(RenderOptions::ink_bg; yellow -> ~#4a4a10), and text drawn on them
(Draw::Glyphs on_text_bg) takes the lighter of its colour and its inverted
colour (RenderOptions::ink_text), so black and Automatic text draw white
with WCAG contrast >= 4.5 on all 16 highlight colours. Light page, print
and PDF are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1252: text on a dimmed highlight always reaches 4.5:1; lines too
Glyphs and lines now carry the text background they sit on
(Draw::Glyphs/Draw::Line text_bg: Option<Rgb>) instead of a bool. On a
dark page RenderOptions::ink_text takes the lighter of the colour and its
inversion, and falls back to white when that is under 4.5:1 against the
dimmed background (pure blue on a dim olive was 1.34:1). Underlines and
strikethroughs on a highlight use the same rule. The dim constants are
crate-private and documented as assuming the default dark paper grey.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1252: docs: the 4.5:1 rule for text and lines on highlights
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* #1381: WIP
* #1381: HTML import: bold <th>, cell text-align, region paper for HTML/MD/TXT
Header cells are bold unless their own CSS says otherwise. text-align on a
td/th, or inherited from its tr/row group/table, aligns the cell's
paragraphs; a paragraph's own alignment wins, and a table's align attribute
(which places the table) no longer counts. Files that give no page size
(HTML, Markdown, plain text) open on the region's paper, as a new blank
document does: file.open uses the session's region, wordcraft-cli reads the
system region the way the app does (Region::from_locale_vars, shared with
ui-egui). Tables size to that page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1381: the CLI uses the system region everywhere; help mentions WORDCRAFT_REGION
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- A position wrapped in mc:AlternateContent inside wp:anchor (wp14:pctPosVOffset)
takes the Fallback's wp:posOffset; the shapes were placed at the top of the column.
- A custGeom path that ends where it started without a:close is closed, so it's
filled as in Word; the cover's panel (and its white title) was invisible.
- Credits: add benandez to contributors/people.toml.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Ctrl+A stays on this page, and in a table grows cell, then row, then table, then the document (#1424)
Fixes#1424
* Table: Option::map for the outer-cell selection (clippy::manual_map on Rust 1.99)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Fields: PAGEREF in headers and footers shows its bookmark's page
Headers and footers are laid out once the body is paginated, so a page
reference there now shows the number of the page its bookmark starts on, in
that page's number format, instead of the result stored in the file. Like the
page totals in headers and footers, it is filled in after pagination and
doesn't make the body lay out again. Mail-merged letters use this for
"Page X of Y" per letter (Y = PAGEREF to a bookmark at the letter's end). Page
references in the body keep their result until fields are updated.
* Contributors: add cyril265
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Shapes: rounded rectangles keep their own corner radius
A roundRect's adj (its corner in 100000ths of the shorter side) is read, kept
on the shape, saved back, and used for the screen, the PDF and Tight/Through
wrapping. Without one the corner stays at 16%.
* Render: test rounded rectangle corners through layout and rendering
* Docs: progress log and file-format parity for rounded rectangle corners
* Render: pass the shape's corner to the caret-background hit test (#299 code from main)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* PDF: draw horizontal and vertical lines inside groups
A line or connector inside a group keeps its zero height or width (a:ext cy="0"),
and the PDF export skipped every shape without area, so dividers drawn in Word
groups were missing from exported PDFs while the screen showed them.
* PDF: test hostile sizes for flat lines
* Docs: progress log and file-format parity for lines inside groups in PDF export
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Update Table took an entry's text from the heading's plain text, which has
no list label, so headings numbered through a multilevel list (1, 1.1)
lost their numbers in the TOC. The entries now walk the body's list
counters and put the label and the level's separator (tab, space or
nothing) before the text, as Word writes it.
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
"Title Bar" was "Címsor", the word the catalog uses for Heading (Címsor 1, Címsor 2); it is now
"Címsáv". "AutoFit Column by Double-Clicking" now uses "automatikus méretezés", as AutoFit
reads elsewhere in the catalog.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Metafile: WMF handle table finds its lowest free slot without scanning (#1463)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Metafile: map shape ops in place and decode only the clipped part of a DIB (#1463)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* ROADMAP: progress log row for the metafile reader (#1463)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Metafile: write cropped DIB pixels through as_chunks_mut (#1463)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A horizontal cell edge ran from cell edge to cell edge, so at a thick
vertical edge it started on that edge's centre line and left a notch half
the border width wide at every corner. Horizontal edges now run on over
half of each visible vertical edge, as Word draws them.
Co-authored-by: dfgodvan <180286157+dfgodvan@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* i18n: add Vietnamese (vi) interface translation (#1434)
* vi: translate the strings main gained since the catalog was drafted
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Math: bound the depth the equation parsers build (#1456)
x^1^1^1…, a/a/a… and \sum\sum… built left-nested trees thousands of levels deep within the
input cap; clean, Clone, PartialEq, Drop and serde then overflowed small stacks. Wrapping an
existing node (script base, numerator, accent, limit base, n-ary body) is skipped once it is
already 32 levels deep (iterative check); the operator is text (linear) or dropped (LaTeX).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Math: escape \text{…} in LaTeX output so it reads back unchanged (#1456)
Normal-text runs were written verbatim, so a}b\c became \text{a}b\c} and read back as ab\c.
Escape \ { } % $ # & _ ~ (backslash as \backslash{}); \backslash is now a named symbol and
raw_arg ignores the empty braces after a control word. A # inside normal text no longer
numbers the equation.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Math: merge runs inside every structure, not just some (#1456)
merge_runs only recursed into fractions, scripts, radicals, n-ary operators, delimiters and
functions, so the LaTeX parser left \overline{ab} as two runs where the linear parser gives
one. It now recurses through map_args for every node kind.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Space and Enter count as a click on the focused canvas. request_focus
always interrupts IME, and on X11 winit then destroys the XIM context.
fcitx swallows every later key. Ignore that keyboard click. Pointer
clicks, accessibility clicks, and drags still focus the canvas.
- Every cell in a row gets the row's largest top and bottom cell margin, so
a cell without its own margins lines up with its neighbours.
- An exact row height covers the top border, top margin and text; the
bottom margin comes on top. Dragging a row border, or drawing a line
through an exact row, accounts for it.
- A row split across pages must keep its cells' bottom margin, border and
last paragraph's space after on the first page, and widow/orphan control
applies to the paragraphs in its cells. A row whose text fits but whose
margin doesn't no longer moves to the next page whole.
- Each cell of a split row goes on at the top of the continuation (one
cell's widow line no longer pushes its neighbours down), and the
continuation is as tall as the cell needing the most room, and at least
the rest of the row (a minimum row height holds over the pages).
- A nested table's cell the page bottom cuts through goes on from the cut
with what is left of it, and the text after it stays below it.
- A row of an exact height never splits: it moves to the next page whole.
Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>