* #1077: forms protection fills content controls (w:edit="forms")
* #1077: forms walk stays conservative for unclosed control markers
* #1077: forms protection edges — nested locks, orphan markers, tables, empty interiors
* #1077: forms protection — formFields state and Find Next Region over fillable controls
* #1077: forms protection round-trip, password guard, docs
* #1077: final review fixes — lock-param gate, per-list pairing, docs, pane perf
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Harden against hostile-document DoS and non-web link export
A security pass found several small files that make the app allocate or
compute without bound when opened (GUI, wordcraft-cli, or the MCP server),
and link targets leaving on export without the scheme check the HTML and
Markdown writers already apply. None lets a document run code or read files;
all are denial-of-service or credential-leak-on-click. Each fix bounds an
attacker-controlled size and lands with a regression test, per the
never-crash standard.
- layout: a table repeats its header rows on every page it spills onto, so
many tall header rows paginate to O(headers x pages) placed items. Cap the
total header-row repeats per table (lib.rs). 2 KB docx: 1.1 GB -> 124 MB.
- formats/model, html: an HTML table of many <tr> with large rowspan expanded
to rows x 64 covered cells, and `model::table` pre-allocated a full
rows x cols grid only to clear it. Cap rows at MAX_ROWS before
insert_covered, bound its total work, and build the grid template with one
row. 640 KB html: >1.5 GB -> 214 MB (a plain large table is faster too).
- docx/read/freeform: a custom-geometry Bezier segment took any number of
points, making de Casteljau flattening quadratic. Cap points per segment.
3 KB docx: 12 s -> 0.15 s.
- docbin: footnote/header/text-box story tables and the piece table can name
the same backing text many times. Hold the total characters materialised to
twice the WordDocument stream size (a ceiling no real file reaches, since
each char costs >=1 stream byte). 45 KB doc: >1.2 GB -> 14 MB.
- render, pdf, formats/rtf: document pictures were decoded with no dimension
limit, so a tiny file declaring a huge image could allocate before the size
check. Decode through image::Limits with a side cap first.
- doc/link, formats/rtf, odt, latex: route exported hyperlink targets through
is_safe_href (as HTML/Markdown already do), and extend it to reject
protocol-relative (//host) and UNC (\host) references. Keeps file:,
javascript:, ms-msdt: and UNC targets from being written into exported RTF,
ODT, LaTeX and PDF where a reader would follow them (UNC leaks NTLM on
Windows).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Align picture-decode limits with the bounds already in force
Review of the previous commit: `image::load_from_memory` and
`load_from_memory_with_format` already applied `Limits::default()`, whose
512 MB allocation cap bounded every picture decode. The PDF change had set
`max_alloc = None`, loosening that; restore the default. The render and RTF
side caps (16 384 px) newly rejected wide-but-short pictures that fit in
memory, so render now uses the pixmap's own `u16` side limit (what
`premul_pixmap` enforced after decoding, applied before it instead) and the
RTF re-encode keeps only the explicit allocation cap. Net effect: rejection
moves to header time with no change to the sizes accepted. Also fix a
comment: the header-row repeat cap is per table, not per document.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Word forms keep their legacy form fields and rows' skipped grid columns when saved (part of #1174)
* Tests: field initializers name the new form field
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M10: retain local crash recovery on current main
* i18n: Backstage Recover in every catalog
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* #775: STYLEREF fields in headers and footers show each page's result
Running headers/footers with STYLEREF (e.g. the current chapter title) are
evaluated per page at layout time (ECMA-376 §17.16.5.59): the page's first
paragraph of the style (\l: last), else the nearest before, else after.
Style names/ids, heading levels (STYLEREF 1), character styles, and the
\n \r \w \s \t \p switches. Body STYLEREF fields update with the other
fields. STYLEREF added to Insert › Quick Parts › Field with its options.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #775: STYLEREF error text in our own words
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review fixes for #882: translate the STYLEREF Field dialog strings
Adds the style picker label, the six STYLEREF switch labels and the
STYLEREF description to all 18 interface catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* STYLEREF: reuse the style index while the body and styles are unchanged
Headers with STYLEREF rebuilt and hashed the body's style index on every
layout pass (echelon's follow-up note on #882). The layout cache now keeps
the index with the body blocks it came from (shared, so an edited block is
a different pointer) and the styles/numbering hash, and rebuilds it only
when either changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* vi: STYLEREF field strings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* DOCX: merge settings.xml into the original instead of regenerating it (#1034)
A save regenerated word/settings.xml from the model, so every setting the
model doesn't hold was dropped: compatibility options (lost in 18 of 18
real-world Word files), rsids, document variables, proofing state and the
colour scheme mapping.
The reader now keeps the original part when it holds such settings, and
the writer merges into it: the original root and every unowned child stay
byte for byte; owned children follow the model in CT_Settings order; in
w:compat only compatibilityMode is replaced. The privacy options are the
model's since #1062, so they are owned too. Kept children that point at a
relationship (a mail-merge data source) are dropped. Anything unexpected
falls back to the generated part. Files WordCraft wrote itself keep
nothing extra.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* DOCX: carry unmodelled parts and keep comment ids, style defaults, latent styles, list ids and app props (#1034)
Builds on the settings.xml merge.
- Parts the package or main document relate to that WordCraft neither
models nor writes (webSettings, stylesWithEffects, people, add-in task
panes, the thumbnail, future parts) are carried verbatim with their
relationships and content types; relationship types the writer
produces, and any the body refers to by id, are not (HANDLED_RELS).
- commentsIds / commentsExtensible: durable ids and UTC dates are read
into the model and regenerated against the written paragraph ids.
- w:default on localized default styles, w:latentStyles, lists'
nsid/tmpl/multiLevelType, and app.xml Template/Company/Manager/
HyperlinkBase round-trip; counts are not carried, and Company/Manager
are dropped when the file asks to remove personal information.
- Comment reference marks use the document's "annotation reference"
style id instead of a hard-coded CommentReference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: przemek-arch <263159043+przemek-arch@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.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.
* 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>
* 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>
* #961: Alt+numpad decimal character codes (ANSI, OEM 437/850, Unicode)
Hold Alt, type a decimal code, release Alt: the character is typed at the
caret through the new text.typeCode command. A leading 0 means
Windows-1252, no leading 0 the OEM code page (437, or 850 for Western
European locales), above 255 the Unicode character. Not on macOS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #961: Alt codes follow the system code pages; ignore auto-repeat, drop on blur
text.typeCode takes optional ansi (874, 1250-1258) and oem (437, 737, 850,
852, 857, 866) code pages; OEM 852/866/737/857 tables added from the public
mapping files. On Windows the shell passes the system locale's pages and, for
codes up to 255, lets the character Windows composes replace ours (above 255
ours stays and Windows' byte-reduced text isn't typed). Auto-repeat of a held
digit adds nothing, and losing focus drops a half-typed code.
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>
* #816: copied equations reach other programs as MathML
A copy or cut whose selection holds an equation now also puts HTML on the
system clipboard, with each equation as Presentation MathML (wrapped in
<semantics> with the linear format as an application/x-unicodemath
annotation), so Word, LibreOffice, Google Docs and browsers paste a
formatted equation instead of `y=2x_s`.
- formats: new mathml module, a writer from the equation tree (runs,
fractions, scripts and pre-scripts, radicals, n-ary operators, delimiters,
functions, limits, accents, bars, border boxes, group characters, equation
arrays, matrices, phantoms) and a bounded reader (our annotation, else the
presentation elements, else a TeX annotation). HTML export writes
equations as MathML; HTML import (open and paste) reads <math> back.
- engine: edit.copy/edit.cut (and column copy) return `html` and keep
Session::clipboard_html when the fragment holds an equation.
- ui-egui: Services::set_clipboard_html; copy sites put HTML with the text
through it, falling back to egui's text copy.
- wordcraft: arboard set().html() with the text alternative; on Wayland
without data-control the text-only path is kept.
OMML on the clipboard (Word's "Office Open XML") needs custom clipboard
formats arboard can't set; left as a follow-up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #816: honest clipboard docs; MathML reader fixes (token cap, function names, column vectors)
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>
* #1116: keep a paragraph's space after clear of the footnote separator
On a page with footnotes (already there or brought by the line), a
paragraph's last line only fits if its space after fits above the notes
area too; otherwise it goes to the next page with its note (and, with
widow control, one more line). Contextual spacing and automatic spacing
between list items drop the requirement; pages without notes are
unchanged (space after may still run past the bottom margin).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1116: space after vs footnote separator uses the spacing code's tests, only on pages with notes
Contextual spacing drops a paragraph's space after only when that paragraph
asks for it, and the next paragraph counts as a list item only when it gets
a label (Numbering::shows_label), as in the spacing between paragraphs.
The kept space after is worked out only for a last line on a page with notes.
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>
* #1062: honour Remove personal information / Remove date and time on save
w:removePersonalInformation and w:removeDateAndTime (ECMA-376 §17.15.1.69-70)
are read into Settings, written back in CT_Settings order, and honoured by the
DOCX writer: comment and revision authors saved as "Author" (initials "A"), no
dc:creator / cp:lastModifiedBy, no w:date on comments and tracked changes. The
open document keeps its real values. New file.privacyOptions command sets both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1062: reading privacy options has no side effects; File › Options › Save check boxes
file.privacy reads the settings (a pure command: no undo step, no unsaved changes);
file.privacyOptions only changes them and refuses an empty call. File › Options › Save gets
a "Privacy for this document" section with the two check boxes, translated in every catalog.
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>
* Text boxes: VML text boxes keep their internal margins, fit to their text and alignment
A VML text box's inset ("l,t,r,b", ECMA-376 Part 4 §19.1.2.22) was ignored,
so its text sat Word's default 7.2 pt in and 3.6 pt down even where the file
says 0,0,0,0, which is how letterheads and form headers place address blocks
exactly. It's now read like DrawingML's lIns/tIns/rIns/bIns: each value left
out keeps the default, a bare number is in EMUs, o:insetmode="auto" leaves
the defaults. mso-fit-shape-to-text (VML's "Resize shape to fit text") is read
as auto-fit, like a:spAutoFit, and the shape's v-text-anchor as the text's
vertical alignment. In a group, a member's margins go with that member's
text.
The reader feeds layout, so the editor, PDF and page images all change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Drawings: hidden shapes and pictures are kept but not shown
A drawing marked hidden (VML visibility:hidden, floating or inline;
DrawingML wp:docPr/@hidden, or a group member's cNvPr/@hidden) was drawn,
took room and wrapped text. Word for Windows shows it in no view; it's now
kept and saved hidden, but left out of layout: not placed, no wrap area, no
room in its line, nothing drawn, its text box's notes not placed. Exports to
other formats and copies leave it out, the accessibility checker no longer
asks for its alt text, and the selection pane says it's hidden.
Word for Mac draws hidden VML shapes; Windows follows the markup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
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>
Rgb now reads "RRGGBB" / "#RRGGBB" as well as [r, g, b], so every colour
in CharProps and ParaProps takes the hex that format.color, para.borders
and table.shading take, and {"Rgb": "1B1611"} works for color. It is
still written as [r, g, b], so saved documents and settings are unchanged.
Fixes#865
Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Track Changes: emptying a paragraph no longer turns its mark into the text's revision (#864)
Paragraph::delete copies the formatting of what it deletes onto the paragraph
mark when it empties the paragraph. That copy included the revision ids, so
accepting a tracked deletion of a whole paragraph text made the mark a deleted
mark too, and Accept All joined the following paragraph. Rejecting typed text
in an empty paragraph likewise rejected the paragraph mark. The mark now keeps
its own ins, del and fmt_change.
* Credit Silvertree2010 in the About window
* Docs: update the progress docs for this change
* Docs: Last reviewed stays as it was
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Microsoft Word treats generated numbered-list labels as independent words
in document word statistics, while omitting them from character counts.
Previously, word counting in WordCraft inspected plain run text only,
causing sample_document() to report 357 words instead of Word's 361.
- Add Document::story_word_count and story_word_count_of to compute words directly,
including generated numbered labels while skipping bullet markers and empty labels.
- Handle LevelSuffix::Nothing correctly by counting labels as independent words
without string concatenation artifacts.
- Keep Document::plain_text unadorned to maintain exact run text across docbin,
clipboard, and tests.
- In review.wordCount, report words via story_word_count while preserving character
counts from plain_text.
- Add regression tests covering numbered lists, bullets, LevelSuffix::Nothing, and
review.wordCount word vs character parity.
Fixes#893
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A character colour given as a string now reads the way format.color reads it: RRGGBB, #RRGGBB, or auto/automatic in any case. The serialized forms ("Auto", {"Rgb": [r, g, b]}) read as before and are still what gets written, so documents and settings on disk round-trip unchanged. A colour that isn't one of these is still a parameter error.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
RTF import read every 8-bit byte as Windows-1252, so Cyrillic (and any
other non-Western) RTF came out as mojibake. Bytes now decode in the
current font's code page (\fcharset, \cpg), else the default font's,
else the document's (\ansicpg), for the single-byte Windows code pages
874 and 1250-1258.
A .doc, .docx or .rtf whose content is another of these formats now opens
by its signature, as Word does: RTF saved as .doc (hh.ru résumés and
accounting exports are commonly saved that way), a .docx renamed .doc, a
Word 97-2003 file renamed .docx.
Co-authored-by: Магда М.Г. <mail@mgmagda.ru>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Word's symbol fonts are proprietary and never bundled, so text set in them
showed the substitute's letters. New wordcraft_doc::symbol_fonts maps every
code (stored as U+F020-U+F0FF, e.g. w:sym, or as a plain 8-bit code) to its
Unicode equivalent: Symbol from Unicode's Adobe symbol.txt, the Wingdings
and Webdings sets from Unicode L2/12-368 (WG2 N4384), checked against UCD.
Layout shapes the mapped characters (fallback faces draw them); PDF text,
copy as text and TXT export read as the symbols; the stored text and docx
round trips are unchanged.
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>
Found by rendering 12 US government Word documents (NIH, EPA, MCC) in
Word for Windows and WordCraft and comparing the pages side by side,
then pinning each rule down with synthetic documents measured in Word.
- Kerning: the shaper kerned every run, so WordCraft's lines held more
text than Word's and broke differently. Word kerns only a run with
Kerning for fonts (w:kern) at or above that size; no w:kern and
w:kern="0" are unkerned. New documents keep kerning from 1 pt, like
Word's Normal template.
- Automatic paragraph spacing (w:beforeAutospacing/w:afterAutospacing,
common in text pasted from web pages) was ignored. It is 14 pt at
any size, none before the document's or a cell's first paragraph,
none after a cell's last, and the larger of two neighbouring spaces
counts. Read, written back, and settable through para.spacing
(beforeAuto/afterAuto); a number, Ctrl+0 and Remove Space After
replace it as in Word.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A numbered paragraph style's own indents (and those of styles based on it)
now beat the list level's, its base styles' don't, and the paragraph's own
list beats every style. A paragraph that switches its list off (numId 0)
drops the styles' indents, as Word does; the first Backspace in a List
Paragraph item makes the item's indent the paragraph's own so the text stays
put. Measured in Word for Windows: 27 synthetic cases, all match. NIH's
Heading 3s sit at their 0.9" and its "sample text" paragraphs at the margin.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Found in NIH's monitoring-plan template compared with Word for Windows:
its footer puts the page number in a text frame (w:framePr: centred,
text wrapping around it), and Word draws "NCCIH DSMP Template" ending
just left of the number on the same line.
Before: frames other than drop caps were ignored on import, so the
number took a line of its own above the footer text (making every page
a line shorter), and the frame was lost on save.
After: a paragraph's text frame is read (size, anchors, alignment,
offsets, wrap, distances) and written back. In headers, footers, table
cells and text boxes it is placed like a floating object, sized to its
text unless it has a width, and the text after it wraps around it; the
DSMP footer now matches Word's. Frames in the body text still lay out
in line.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Read and write w:hideMark on table cells, and lay it out like Word: a cell's
empty last paragraph takes no room, spacing and all, so a row of only such
empty cells is just its borders; an ordinary empty cell keeps the row a line
tall. Measured in Word for Windows: 9 synthetic cases. NIH's Table A no
longer shows a blank row.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* #836, #837: language-aware sorting and locale numbers in tables
Sorts (Table > Sort, Home > Sort, index and its letter headings, bibliography,
Table of Authorities) use the text language's alphabet via a small pure-Rust
collation (crates/doc/src/collate.rs). Table sort and formulas read numbers
with the cell language's decimal/group separators and write formula results
with its decimal separator (crates/doc/src/locale_number.rs). Sort comparators
are now total orders.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #836, #837 review: ordinals, leading zeros, percentages; untagged text; decomposed letters
- parse_number: a trailing separator with no digits is the whole number (`1.`, a
numbered "Lp." column, sorts as 1); a first group of `0` is never grouping
(`0.125` in Polish is 0.125, not 125); a trailing `%` is ignored (`12 %` is 12);
an en dash is a minus.
- Text with no language follows the document default, else the editing language
(`cmd::default_lang`), as AutoCorrect does: a Polish interface reads `3,50` as
3.5 in an untagged document. Table sort and formulas, paragraph sort, the
index, the bibliography and the Table of Authorities use it.
- Collation composes a letter and a following combining mark first (table from
Unicode NFC), so a decomposed `ż` (z + U+0307) sorts as `ż`.
- Tests: every Polish letter and its index heading, decomposed letters, the
ordinal column, an untagged Polish SUM, Polish bibliography order.
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 Word 365 cover page lost its look. The missing fills came from
theme-coloured shapes (scheme colours and wps:style fill references),
which the reporter's 0.5.0 build didn't read; #611 fixed that on main.
Two causes were still open on main:
- A freeform (a:custGeom) inside a wpg group was drawn as a rectangle.
read_wsp now reads custom geometry, so group members get it as well.
- Text boxes ignored wps:bodyPr lIns/tIns/rIns/bIns and a:spAutoFit.
Layout always used Word's default margins, so a title box with no
margins that was just tall enough for its text lost its second line.
TextBody now carries the box's own margins and auto_fit. Layout uses
those margins and doesn't cut the text of a box that resizes to fit.
Both are written back on save.
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>
* Layout: an inline picture wider than the text keeps its size, like Word
WordCraft shrank an inline picture, chart or shape that is wider than
its line to the line's width, height with it. Word draws it at its own
size: it runs past the margin and the page edge clips it, and the line
is as tall as the picture. In docxide-pdf's fixtures that moved the rest
of the page up (case179: a 1014 x 100 pt picture became 468 x 46 pt) and
shrank a header's 1014 pt logo strip to fit (alfies_arc).
All page outputs already clip at the page: the PDF page box, and the
page bitmaps the rasteriser and the editor draw. Text boxes don't clip
what runs past their sides yet, so a picture wider than a text box is
still fitted to the box rather than spilling over the page
(ParaEnv::fit_objects, part of the paragraph cache key).
On the 390 fixtures: case179 SSIM 70.9 -> 99.9%, alfies_arc 18.4 ->
74.8%, case178 98.0 -> 99.9%, nabl_lab 33.2 -> 36.8%; no fixture got
worse.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review fixes for #745: fit imported and pasted pictures to the text width
Layout no longer shrinks oversized inline pictures, so pictures that come at their pixel
size are fitted when they enter the document, as Insert › Pictures already does:
- opening HTML (also from a path), Markdown, ODT, RTF and other formats-crate imports
(io::fit_pictures, against the narrowest section text width);
- pasting HTML/RTF (paste::adopt, against our text width).
Only inline pictures wider than the text are scaled, proportionally. DOCX keeps its sizes.
Test: tests::imported_pictures_fit_the_text_width.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
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>
* Bidi: auto-detect RTL paragraph direction and keep typing caret advancing smoothly
When typing in Kurdish, Arabic or Hebrew in a new document, default
paragraph direction (bidi: None) was resolving to LTR. In an LTR
paragraph, visual reordering pinned newly typed characters and the
caret to the left margin (x = 72.0), resetting to the start of the line
with every keystroke.
- Add first_strong_is_rtl() following UAX #9 rules P2/P3.
- Auto-resolve unassigned paragraph direction to RTL in layout when the
paragraph starts with an RTL script.
- Auto-promote paragraph to RTL (bidi = Some(true)) when typing text
starting with an RTL character.
- Add regression test in wordcraft-engine.
* Review fixes for #713: RTL only when typing into an empty paragraph
- Drop the layout fallback that drew any paragraph without w:bidi as RTL when its first
strong character is Arabic/Hebrew: opened DOCX paragraphs keep their direction, and a
style that sets bidi=false is respected.
- type_text promotes a paragraph to RTL only when it had no letters before the insert and
nothing (paragraph, style chain, document defaults) sets its direction; typing into a
paragraph that already has text no longer flips it.
- docs/typing-parity.md: Text direction section; test in tests_typing.rs.
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>
Keep inherited complex-script size, bold and italic independent from Latin
properties and retain explicit values during DOCX round trips. Preserve
ordinary formatting-command behavior and add regression coverage.
Resolve eight existing Clippy warnings without changing behavior so all
six required CI gates pass.
Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M8.1: Bibliography sources DOCX round-trip
Store bibliography sources in package-root custom XML (customXml/item1.xml) with datastore item properties (customXml/itemProps1.xml), relationships, root customXml relationship (_rels/.rels), and content type overrides per ECMA-376 §15.2.
Map internal source types to Word b:SourceType values (Book, BookSection, JournalArticle, ArticleInAPeriodical, InternetSite, DocumentFromInternetSite, ElectronicSource, Report, ConferenceProceedings, Case, Art, Film, Interview, Patent, Performance, SoundRecording, Standard, Thesis, Misc) and parse them back. Write SelectedStyle and StyleName attributes on b:Sources. Use safe slice indexing for author names parsing. Scan customXml relationships and customXml/item*.xml on read, extracting sources from b:Sources in the bibliography namespace.
Add roundtrip integration tests in crates/docx/tests/roundtrip.rs verifying all parts, relationships, and attributes, update second-generation kitchen sink stability test, add hostile-input tests in crates/docx/tests/malformed.rs, and verify integration with citation styles in engine citations test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Review fixes for #532: bounded source reading, real citation style
- Read at most 10,000 bibliography sources and dedupe them with hash sets, so a hostile
file with ~100k b:Source elements can't make opening quadratic; each custom XML part is
read at most once (64 parts at most). Test: hostile_bibliography_source_count_is_capped.
- Drop the rt::BIBLIOGRAPHY relationship branch (not an OPC relationship type) and the
stray comment in write_as.
- Write the document's citation style (References > Style) in b:Sources/@StyleName and
@SelectedStyle instead of always APA: Settings::bib_style records it, the style
command sets it, opening a file reads it back into the session.
- The b: namespace goes with the other non-body namespaces (it isn't declared on
document.xml), matching #612.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Keep the custom XML properties relationship apart from customXml
So #612, which adds the same rt::CUSTOM_XML line, merges with this branch without a
conflict.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* clippy: pass parse_persons by value (needless_borrows_for_generic_args)
* rustfmt after merging main
---------
Co-authored-by: yefan911 <yefan911@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- `ui.render` and `ui.screenshot` without a `path` return the PNG as base64 `png` in the
reply instead of writing a fixed name into the shared temporary folder, where another
user could have planted a link to one of this user's files; the MCP bridge's
`render_page` and `screenshot` use that and no longer round-trip through
`temp_dir()/wordcraft-mcp-*.png`. A given `path` still works as before (#678).
- One allow-list, `wordcraft_doc::link`: `is_web_or_mail_link` for Ctrl/⌘-click and
`is_safe_href` (web, mail, `#bookmark`, relative path; schemes read the way browsers
do, so `java\nscript:` is a script) for the HTML and Markdown exporters and the HTML
importer. Scripts and other schemes export as plain text (#679).
- Tests: the allow-list, exporters with hostile links, the MCP bridge against a fake app
(no `path` ever asked for), the control channel's in-reply PNGs.
Co-authored-by: petycomppety <30414255+petycomppety@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- Indents in characters (w:firstLineChars, w:hangingChars, w:leftChars,
w:rightChars; sprmPDxc* in .doc) are kept in the model, read and written,
and laid out in the paragraph mark's font size, winning over the points
as in Word. Indent commands cancel an inherited character indent.
- .doc: sprmPDxaLeft1 has a two-byte operand (was read as four, so every
first-line indent was lost); sprmPDxaLeft1_80 is read; LSPD line spacing
is decoded as signed dyaLine + fMultLinespace (exact 20pt read as 799pt,
a page per empty paragraph).
- A floating table placed on the page or margin that would cover the text
already on the page and doesn't fit below it moves to the next page.
- A line holding only a page break stays at the bottom of a full page
instead of leaving a blank page.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Numbering: Chinese, Japanese and Korean number formats (一、二、三 …) (#534)
A list with w:numFmt="chineseCounting" and lvlText "%1、" showed "2、", and the
digit was drawn with glyph ids of the CJK font that supplied the comma, so it
came out as an unrelated letter: the list label was laid out in font runs but
drawn with the style of its last run only.
- NumFormat knows every ST_NumberFormat value (ECMA-376 §17.18.59), so a file
saves back the format it was opened with; unknown values are decimal.
- New doc::cjk_numbers: Chinese/Taiwanese counting (with 十 below 100, digit by
digit above) and counting-thousand (百/千/万/亿, 零 for gaps), simplified and
traditional legal numerals, Japanese counting/legal/digital ten-thousand,
Korean counting/digital/legal, ideograph digits, heavenly stems, earthly
branches and the sixty-year cycle, aiueo/iroha (half and full width),
ganada/chosung, enclosed numbers (①, ⒈, ⑴, ㊀) and full-width digits. Values
past a sequence's end fall back to decimal.
- Legal numbering (w:isLgl) shows every level, the current one too, in decimal.
- List labels keep their font runs and draw each with its own face.
- A list level without w:ind no longer gets a 36pt indent on read.
- .doc number format codes 0x00-0x27 map to the formats (0x16 decimalZero).
- PAGE/NUMPAGES fields take \* ChineseNum1/2/3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* rustfmt after merging main
* Layout test: end the list range inside the third paragraph (#477 skips a paragraph the range ends at the start of)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- Model (doc::smart_art): layout, items (text + level), colour variant; capped at 100 items,
255 characters, 8 levels.
- Layout algorithms of our own (docx::smart_art) place rounded rects, circles, bands, arrows and
connectors; WordCraft draws the graphic from the same dsp:drawing it saves.
- DOCX: data (dgm:dataModel points and connections), our own layout/quick-style/colour
definitions under the built-in identifiers, the drawing part, relationships and content types.
A diagram reads back editable only when its data part is exactly what WordCraft writes.
- Commands: insert.smartArt, smartArt.items/addShape/promote/demote/layout/colors/reset/textPane,
one undo step each; picture.size relays out a SmartArt graphic.
- UI: layout picker with previews drawn in code, SmartArt Design tab, Text Pane; strings in all
9 catalogs.
- Diagram reader draws `line` presets top-left to bottom-right (was the other diagonal).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- Model: SectionProps.text_direction (horizontal, tbRl, btLr); unknown
values read as horizontal.
- DOCX: w:textDirection in w:sectPr read and written in CT_SectPr order
(after w:titlePg, before w:bidi), also inside w:sectPrChange.
- Layout: a vertical section is laid out on its page turned on its side
(margins, page borders and columns turned with it) and each page is
turned onto the real page: tbRl lines run top to bottom stacking from
the right, btLr bottom to top from the left. Pictures, shapes and text
boxes stay upright; headers and footers stay horizontal; a section
turning the other way starts a new page.
- Display: in top-to-bottom text ideographs, kana, Hangul and full-width
forms stand upright (ideographic comma/full stop moved to the upper
right); Latin lies on its side. Raster and PDF draw it unchanged.
- Hit testing: clicks outside turned lines find the nearest turned line.
- Command layout.textDirection (Layout › Page Setup › Text Direction):
turns the selected cells or the text box the caret is in, else the
section; ribbon menu and keytip.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Templates and Add-ins: attached template, update styles, Organizer (#377)
- Model + DOCX: Settings keep the attached template (w:attachedTemplate's external
relationship of the settings part) and w:linkStyles; both round-trip, written in
CT_Settings order (after w:mirrorMargins, before w:trackRevisions).
- Engine: tools.templates (state), tools.attachTemplate, tools.detachTemplate,
tools.linkStyles, tools.updateStyles (copy the template's styles and defaults, one
undo step; path or base64 data), styles.organizer (list), styles.copyFrom (copies
missing bases, linked halves and the lists a style numbers with), styles.rename.
The template path comes from the document, so it is never read automatically (not on
open either): only an explicit tools.updateStyles / attach with update reads it.
- UI: View > Templates > Templates and Add-ins dialog (path, Attach..., automatically
update document styles, Organizer...) and a two-list Organizer (Copy, Delete,
Rename...); strings in every catalog, Estonian included.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Review fixes for #382: never follow a template path to another machine
A document's w:attachedTemplate can name \\host\share\T.dotx, //host/share or
file://host/share. Opening it (Update Styles, Organizer) would make Windows
authenticate to that host over SMB and leak the user's NTLM credentials.
local_path now returns None for network paths; regression test added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #377: refresh catalog count and parity checklist after rebase
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- Model: `ParaProps::mirror_indents` (`w:mirrorIndents`, ECMA-376 §17.3.1.18), read and
written in DOCX; resolved `mirror_indents` and `suppress_line_numbers` (so styles count).
- Layout: a paragraph with mirrored indents moves over by its indents' difference on even
(left-hand) pages, so its left (inside) indent is on the right there.
- Paragraph dialog: Mirror indents (Left/Right become Inside/Outside), Don't add space between
paragraphs of the same style, and Formatting exceptions (Suppress line numbers, Don't
hyphenate) on Line and Page Breaks; OK sends only the options that changed, in the same
`para.set` as the indents and spacing.
- Strings in all nine catalogs; Reveal Formatting names mirrorIndents.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The font database read only the English family name of each font, so a
Chinese interface listed SimSun, Microsoft YaHei and WenQuanYi Zen Hei
by their English names and never as 宋体, 微软雅黑 or 文泉驿正黑, and a
document naming a font by its Chinese name fell back to another font.
- fonts: keep each face's localized family names (name IDs 16 and 1,
every face of a collection, read during the scan without loading the
file); FontDb::families_in(lang) lists them in place of the English
names for a matching interface language (zh-hans, zh-hant, ja, ko);
face, has_family and styles accept either name.
- ui: the ribbon and mini toolbar font boxes show the interface
language's names and store the English name.
- East Asian font: choosing a Chinese, Japanese or Korean font also sets
it as the East Asian font (w:eastAsia), read and written in DOCX;
layout draws CJK text in a run's installed East Asian font.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>