Commit Graph
201 Commits
Author SHA1 Message Date
aiongg f2b06167aa Integration: #2244 VML on/off values read case-insensitively by the shape fill reader
Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.
2026-10-22 02:05:32 +00:00
aiongg e1e258e4d9 Integration: track t08
contributors/people.toml: union. crates/layout/src/tests.rs: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged.
2026-10-22 01:47:56 +00:00
aiongg 3581f80251 Integration: track t07
# Conflicts:
#	ROADMAP.md
#	docs/file-format-parity.md
#	docs/gaps.md
#	docs/layout-parity.md
2026-10-22 01:47:46 +00:00
aiongg 04b3881e3e Integration: track t06
docs/gaps.md row 4: one row (t06 had kept a second, older copy of it); t05's #1840 and t06's #2964 clauses both in, main's #2963 clause kept. docs/recovery.md: t06's retry sentence with t01's base64 size text. Test files: both tracks' tests kept whole.
2026-10-22 01:47:34 +00:00
aiongg 16d1c8a933 Integration: track t05
crates/pdf/src/lib.rs: t01's vector_text kept before t05's watermark (box area signature). Test files and canvas.rs tests: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged.
2026-10-22 01:46:47 +00:00
aiongg 0d3b8eb0cb Integration: track t04
crates/formats/src/odt.rs: t01's Body::new/take_blocks kept; take_blocks finishes tables with t04's TableB::finish (column widths). Test files: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged.
2026-10-22 01:46:17 +00:00
aiongg 86df1e70ce Integration: track t03
# Conflicts:
#	ROADMAP.md
#	crates/formats/src/html.rs
#	docs/gaps.md
#	docs/ui-parity.md
2026-10-22 01:44:54 +00:00
aiongg d4debe0592 Integration: cargo fmt 2026-10-22 01:34:25 +00:00
aiongg ef00b55870 Merge PR #3429: #3415: banded rows shade the same rows in the reference app and WordCraft (table style band sizes)
crates/docx/tests/fixtures.rs: union, keeps main's shape-gradient test and the PR's band-size tests.
ROADMAP.md, docs/file-format-parity.md: main's status lines with this change added to the newest; log rows from both sides in date order.
2026-10-22 01:25:02 +00:00
aiongg 61570835bc Merge PR #3156: Watermark: the text fills its shape's box, as the reference app's textpath does
crates/docx/src/read/story.rs: main's vml_opacity with the PR's box size.
crates/pdf/src/tests.rs: both sides' tests kept; the glyph_frames helper both #3155 and #3156 add stays once.
2026-10-22 01:24:10 +00:00
aiongg 2dc0e4adf7 Merge PR #3440: A document with many content controls saves in the time they take
ROADMAP.md: status line once (this change first, then main's); progress-log entries and history rows union
docs/file-format-parity.md: status line once; history rows union in date order
docs/gaps.md: status line once (this change first, then main's)
2026-10-22 01:23:17 +00:00
aiongg d64ce28903 Merge PR #3204: DOCX: majorBidi/minorBidi theme fonts follow the theme font language
docs/file-format-parity.md: status line once (this change first, then main's); history rows union
2026-10-22 01:23:01 +00:00
aiongg 1c757ece6b Merge PR #3199: Signature lines stay signable after saving a .docx
ROADMAP.md: status line once (this change first, then main's); progress-log rows union in date order
docs/file-format-parity.md: status line once; VML row merged word by word (main's #1500 fill text plus the PR's signature-line text)
2026-10-22 01:22:51 +00:00
aiongg 3f52369269 Merge PR #2453: DOCX: preserve text boxes containing pictures
ROADMAP.md, docs/file-format-parity.md, docs/gaps.md: main's status lines kept; the PR's log rows added.
2026-10-22 01:22:24 +00:00
aiongg ec81170136 Merge PR #2254: fix(docx): inspect textbox style for v-text-anchor and handle case-insensitivity
crates/docx/tests/fixtures.rs: both sides' appended tests kept.
2026-10-22 01:21:32 +00:00
aiongg eedcd7e98c Merge PR #2244: fix(docx, doc): support VML wrap side and case-insensitive float anchors
crates/docx/tests/fixtures.rs: both sides' appended tests kept.
2026-10-22 01:21:19 +00:00
aiongg d1182d5f8d Merge PR #2477: DOCX: HYPERLINK fields read as links, so TOCs from the reference app's web version keep their tabs and page fields (#2174)
ROADMAP.md: main's status lines; both progress-log and revision rows kept.
docs/file-format-parity.md: main's status line and rows; the PR's HYPERLINK sentence added to both of main's Fields rows; revision rows unioned.
2026-10-22 01:21:08 +00:00
aiongg d5547f80ec Merge PR #2267: fix(docx): correctly parse emu and case-insensitive units in measure
crates/docx/src/units.rs: union of test assertions (main's line_multiple, the PR's emu/case ones)
2026-10-22 01:20:53 +00:00
aiongg 467ad8e279 Merge PR #2460: DOCX: a text frame in the paragraph defaults frames nothing (#2360)
ROADMAP.md, docs/file-format-parity.md: status lines keep main's with the PR's item added; log rows union.
2026-10-22 01:20:37 +00:00
aiongg 8077310218 Merge PR #3463: fix(docx): preserve East Asian language w:eastAsia on w:lang 2026-10-22 01:20:11 +00:00
aiongg 60b4d57a9b Merge PR #3260: Unstyle TOC entry links whose Hyperlink style has a localized id 2026-10-22 01:20:10 +00:00
aiongg d21c8825c2 Merge PR #2429: DOCX: keep the reference app's Table of Contents content control around a TOC the writer doesn't rebuild 2026-10-22 01:19:47 +00:00
aiongg 50ef30939f Merge PR #2034: Floating objects are drawn in their stacking order (relativeHeight) 2026-10-22 01:19:45 +00:00
aiongg b9666f7ff8 Merge PR #1892: Charts: draw radar charts instead of a placeholder (#1890) 2026-10-22 01:19:43 +00:00
5a6d2b6070 File › New: personal templates from a user templates folder (#2852) (#3133)
* #2852: personal templates in File › New, Save as Word Template

File › New lists the .dotx/.dotm files in the user templates folder under
Personal (a subfolder per category, search covers them); a click makes an
untitled document attached to the template. Save As › Save as Word Template
starts in that folder (<settings>/Templates, made on first use; File ›
Options › Advanced › File locations › User templates overrides it).
Engine: file.listTemplates (bounded, network folders refused, read root
applies), file.newFromTemplate sets the attached template, file.options
userTemplatesFolder, file.saveTemplate keeps a .dotm name. UI:
ui.saveAsTemplate; Save dialog requests carry a start folder.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr

* #2852: review — a personal template asks about unsaved changes; network templates folder refused before it's looked at; New lists the folder once per visit with Refresh; .dotx/.dotm labels alike

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
2026-10-21 20:18:50 -04:00
Dylan M. Taylor 7f38f41d8c Use chrono instead of hand-rolled UTC conversion (#3427)
* Use chrono for UTC/calendar conversion instead of hand-rolled copies

Replaces the hand-rolled civil_from_days / days_from_civil copies and the
Excel serial-date, FILETIME and logging/PDF date conversions with chrono as
a workspace dependency (default-features = false, std only, so wasm is
unaffected). Output formats are unchanged and pinned by the existing (and one
new) test. Excel's 1900 leap-year bug and the 1900/1904 systems keep their
explicit special cases; a small proleptic-Gregorian fallback covers years
outside chrono's NaiveDate range so no input changes behaviour or panics.
AGENTS.md now says time goes through chrono.

Follow-up to storytold/cadcraft#1785

* Never panic or reshape date output for clocks outside chrono's range

chrono's From<SystemTime> unwraps, so a clock past year 262143 panicked
inside the logger. logging::timestamp now uses the fallible
DateTime::from_timestamp and reads such a clock as the epoch, as the old
saturating code did.

iso_from_unix_secs fell back to the Unix epoch for timestamps chrono
rejects; it now keeps the proleptic Gregorian date civil_from_days gives,
restoring the pre-chrono result for supported out-of-range u64 inputs.

Years past 9999 are written with {:04} rather than %Y (which prepends a
'+') in logging::timestamp, iso_from_unix_secs, docbin::filetime_iso and
odt::now_iso, so byte-for-byte output is preserved.

filetime_iso gained a docstring; AGENTS.md's chrono bullet now notes the
narrow out-of-range fallback. Tests cover each boundary.

* Don't build an unrepresentable SystemTime in the far-future test

UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.

* Route out-of-range dates through chrono's 400-year cycle, not hand-rolled copies

The PR kept hand-rolled Hinnant `civil_fallback`/`days_from_civil_fallback`
copies for dates outside chrono's ±262143-year range, and AGENTS.md carved a
"narrow fallback" exception for them. That left calendar arithmetic outside
chrono after all.

Replace every copy with the Gregorian 400-year cycle (146_097 days): chrono
converts the day/year within one cycle and whole cycles are added back via i128
(so i64::MIN/MAX can't overflow). This keeps chrono as the only calendar logic
and reproduces the pre-chrono proleptic Gregorian result for every value:

- engine cmd/mod.rs `civil_from_days` (reached by the system clock and by
  `iso_from_unix_secs` for a u64 seconds count past chrono's range).
- engine cmd/controls.rs `days_from_civil` and `days_in_month` (`days_in_month`
  now uses `rem_euclid(400)` since month lengths repeat every 400 years).
- formats sheet.rs `civil` and docx chart.rs `civil` (Excel serials).

Outputs are unchanged: verified the cycle result equals the old formula over a
wide range and at the extremes with a scratch chrono program. New tests pin
accepted inputs past chrono's range on both sides of the epoch, through the
public/lib entry points and the private converters, with values from the old
formula. Removed the AGENTS.md "narrow fallback" exception.

pdf/lib.rs `civil` is left as-is: its only caller is the live system clock and
its year is clamped to 0..9999 by the PDF DateTime, so the epoch fallback past
year 262143 is the accepted live-clock behavior, not a hand-rolled copy.

Tested: cargo test -p wordcraft-engine -p wordcraft-formats -p wordcraft-docx;
fmt --check; clippy --all-targets -D warnings.

* logging/docbin/odt/pdf: keep extended-year dates, no epoch fallback

FredApps (CHANGES_REQUESTED) showed `DateTime::from_timestamp(..).unwrap_or_default()`
changes output: a timestamp the old code converted past chrono's year 262143 range now
collapsed to 1970-01-01 instead of its real proleptic-Gregorian date. The same regression
exists wherever the PR swapped a hand-rolled converter for the epoch fallback. Fix every
such site to keep chrono as the only calendar logic via the 400-year Gregorian cycle
(146_097 days): chrono converts the day within one cycle, whole cycles carry in the year
(i128, saturated into i64). Verified the cycle result equals the old formula across a wide
range and at the extremes.

- apps/wordcraft/src/logging.rs `timestamp`: added `civil_from_days(u64)` (cycle), kept the
  pre-1970 epoch clamp (`duration_since`) and the milliseconds/time-of-day format exactly.
  Pins: UNIX_EPOCH + 10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z (FredApps' example,
  was 1970-...); the checked_add u64::MAX/2 s fixture now asserts 292277026596-12-04T15:30:07.000Z
  instead of the epoch.
- crates/docbin/src/metadata.rs `filetime_iso`: restored a cycle-based `civil(i64)` instead
  of `from_timestamp(..)?` (which dropped the field past chrono's range). A u64 FILETIME caps
  at ~1.83e12 unix s (year 60056), inside chrono's range, so no reachable value changes; pinned
  the largest FILETIME (u64::MAX) -> 60056-05-28T05:36:10Z to lock the extended-year path.
- crates/formats/src/odt.rs `now_iso`: reuse the crate's existing cycle converter
  `sheet::civil` (made pub(crate)) rather than the epoch fallback.
- crates/pdf/src/lib.rs `civil`: cycle converter; year returned unclamped and the caller's
  existing PDF DateTime 0..=9999 year clamp is kept exactly, so month/day/time of a far-future
  clock survive. Pins: civil(10_000_000_000_000)=(318857,5,20,17,46,40),
  civil(-10_000_000_000_000)=(-314918,8,13,6,13,20), civil(253_402_300_800)=(10000,1,1,0,0,0).

engine `cmd/mod.rs::iso_from_unix_secs` already uses the cycle fallback (last round); unchanged.

* Don't build a year-318857 SystemTime unchecked in the logging test

Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.

* Name the 400-year range extension as the one exception to the time rule

The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.

* Say the library computes month and day; time-of-day is plain arithmetic

Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
2026-10-21 19:36:41 -04:00
6b01aa967f Shape fills: gradients and transparency (#1500) (#1605)
* #1500: shape fills keep gradients and transparency (model, DOCX read/write)

A shape's, text box's and canvas's fill is now a `Fill` (solid with
transparency, or a gradient kept as the file states it) instead of one
opaque colour, so gradients and transparency survive opening and saving.

- doc: `fill::Fill`/`Gradient` with sanitizing for hostile input; `.json`
  keeps writing opaque solid fills as `[r,g,b]` and reads both forms;
  `ShapeExtra::outline_fill` keeps an outline gradient.
- docx read: `a:solidFill` with `a:alpha`, `a:gradFill` (stops, `a:lin`,
  `a:path`/`fillToRect`, `tileRect`, `rotWithShape`, `flip`), theme fill
  styles through `a:fillRef` 1-999/1001+ with `phClr`, `a:grpFill`,
  `satMod`, outline gradients, VML `v:fill` gradients and opacity, canvas
  backgrounds.
- docx write: `a:solidFill` + `a:alpha`, `a:gradFill` as read.
- engine/docbin/layout: call sites; drawing still uses the main colour
  (next commit draws gradients).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* #1500: draw shape gradients and transparency (layout, render, PDF)

- layout: WordArt's paint becomes a shared `display::Paint` (solid,
  linear, radial); `paint_for` lays a fill over the shape's frame
  (linear at its angle, `scaled` honoured; path gradients circular from
  their focus to the farthest corner). Shapes, freeforms and canvas
  backgrounds use it.
- render: one `set_paint` for shapes, paths and WordArt; radial
  gradients; soft edges fade the real fill.
- pdf: axial and radial shadings with per-stop opacity.
- Review fixes: nested groups without a fill pass theirs on to
  `a:grpFill`, text-box contents don't take a group's fill, `phClr`
  transparency only on `phClr` colours, VML focus ends consistent and
  opacity clamped, `rotWithShape`/`scaled` written only when set,
  Shape Outline clears an outline gradient, shared stop sanitizing.
- docs: file-format, layout parity, gaps #1, roadmap progress log.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* #1500: shape.fill sets transparency and gradients on every selected shape

- shape.fill (now cmd/shape_fill.rs) takes {"color", "transparency"?} or {"gradient": {stops,
  type, angle, focus, rotateWithShape}}; a colour alone keeps the transparency; values are
  sanitized; applies to every selected shape, text box and canvas as one undo step.
- Formatting (shape.fill, shape.effects) keeps a Shift+click multi-selection, as in Word.
- view.formatShapePane toggles the Format Shape pane.
- Session::undo_mark/merge_undo_since: a pane drag that applies as it goes undoes in one step.
- Helpers for the pane: params(fill), stop add/remove/move, directions, variations and
  presets (variation and preset colours provisional until compared with Word).
- Catalog: Shape Fill Gradient, Format Shape Pane.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* #1500: Shape Fill › Gradient and the Format Shape pane

- Shape Fill › Gradient: No Gradient, Light and Dark Variations of the shape's colour, More
  Gradients… (opens the pane).
- Format Shape pane (right): No fill / Solid fill (colour, transparency) / Gradient fill
  (presets, type, direction, angle, stops bar with add/remove/drag and arrow keys, per-stop
  colour, position, transparency, rotate with shape). Drags preview live, one undo step.
- Right-click › Format Shape… on shapes, text boxes and canvases.
- Gradient swatches have accessible names; new labels translated (own wording, clean room).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* #1500: preset gradients are our own design (clean room)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* #1500: Gradient variations, starting gradient and directions as Word 365 does them

Observed in Word 365 (behaviour and values only):
- Shape Fill › Gradient: a No Gradient swatch, then 13 Light and 13 Dark Variations in a
  grid five wide, in Word's order; light stops tinted 66/44.5/23.5% with saturation ×1.6,
  dark shaded 30/67.5/100% with ×1.15; linear ones scaled; corners with their tile rectangle.
- Format Shape › Gradient fill starts from the theme's first accent (four stops, 0/74/83/100%).
- Corner directions in the pane get the same tile rectangle; Add/Remove sit by the stops bar.
- shape.fill carries "scaled" and "tile"; docx exports color_modified (lumMod, lumOff, tint,
  shade, satMod) so variations use the reader's own colour maths.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* #1500: review fixes for the shape fill ribbon controls

- Gradient variations are built from the colour the shown variation came from, so picking
  variations in turn, or No Gradient, doesn't drift the colour.
- A drag's undo mark is forgotten while the Format Shape pane is closed (a stale mark could
  merge later undo steps).
- Dragging on the stops bar away from a handle no longer moves the selected stop; the arrow
  keys are taken by the bar (no focus moves or shape nudges).
- Direction and Type changes keep a linear gradient's "scaled" and an imported tile
  rectangle; only a corner direction sets its tile.
- Colour modifiers (lumMod, lumOff, tint, shade, satMod) live on Rgb in the doc crate, shared
  by the DOCX reader and the gradient builders.
- Previews build their stop list once per swatch, not per vertex.
- Translations for the pane's hint, direction and preset names.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* #1500: adapt to main: caret contrast samples gradient fills; upstream tests use Fill/Paint

- Paint::at gives a paint's colour and opacity at a point; the caret's background sampling
  (#299) uses it for shape fills, now gradients or transparent, and the Format Shape
  previews use it too.
- Upstream tests (#886 shapes survive saving, render) build fills as Fill/Paint.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* #1500: adapt to main after the rebase: VML presets, rounded corners, new catalogs

- vml_fill takes the shape's shapetype: a VML preset shape (#1377) takes its fill, opacity and
  gradient from its shapetype when it states none of its own, as it already took its colour.
- Rounded-rectangle corners (#1476) and the gradient paints meet in render, PDF and the caret's
  background sampling; ShapeExtra keeps both `corner` and `outline_fill`.
- Tests that landed on main since build fills as Fill/Paint (VML presets, cover pages, VML
  freeforms, path arrowheads, PDF and render shapes).
- The 40 Shape Fill / Format Shape strings in the hr, ko, sv and vi catalogs, which main
  gained since.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz

* #1500: docs: shape gradients and transparency, Shape Fill › Gradient and the Format Shape pane

Re-applied on main's docs after the rebase: progress log, file-format, layout and UI parity
rows, gaps #1, status lines and revision history; parity checklist regenerated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz

* #1500: a theme picture fill falls back on the reference colour; VML freeform opacity note

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz

* #1500: adapt to main after the second rebase: VML shapetype tests compare Fill; parity checklist regenerated

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz

* #1500: one VML fraction parser; a preset takes its gradient from its shapetype

Review notes: story.rs's watermark/freeform opacity reuses shape_style's vml_fraction (clamped at the callers) instead of a second copy; a test pins a VML preset whose v:fill gradient comes from its v:shapetype, and the 65536ths form of a fraction.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz

* #1500: adapt to main after the third rebase: the shape duplicate preview test fills with Fill

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kKPnx8r1vcYumjyL3kcuz

* #1500: review — harness test: right-click a shape, Format Shape… opens the pane and keeps the shape selected

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr

* #1500: review — theme keeps every fill style a fillRef can reach (999 per list), still bounded

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WEhZXSxzS8orX7f9J3qpr

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
2026-10-21 19:12:26 -04:00
Divyanshu Gupta a8ba49e68b fix(docx): preserve East Asian language w:eastAsia on w:lang
Preserve w:lang/@w:eastAsia when reading and writing DOCX documents.
Previously, only w:val and w:bidi were handled, causing w:eastAsia to be
dropped on save.

Fixes #3457
2026-10-21 20:13:15 +05:30
Mat Strangeandopencode 7f77660e03 A document with many content controls saves in the time they take
Saving a document with many content controls was quadratic in the number of
controls: 32,000 of them took four seconds to save, and doubling that took
four times as long again.

Two loops in the writer cost that. The plan that decides where each control
is written collects the inline markers of every control in one set, and then,
for each paragraph, looks for that paragraph's markers by filtering the whole
set — so one paragraph's answer costs the size of the document's marker set,
and the save costs paragraphs times markers. And to find the block-level
controls that clash, every control was compared with every other control.

The markers are now grouped by paragraph once, so a paragraph's markers are a
lookup, and the block-level controls are grouped by the block they end in,
each group keeping the two earliest closes, which answers the clash question
the same way in one sweep.

What is written does not change: the packages written for ten shapes of
control nesting — inline, around paragraphs, chained, nested — are byte for
byte the same as before.

Tests: crates/docx/tests/content_control_save_cost.rs. The two timing tests
fail without the fix (16.1x and 13.0x the time for four times the controls,
against a bound of 8.0) and pass with it (4.6x).

Fixes #3437.

Co-Authored-By: opencode <noreply@opencode.ai>
2026-10-21 13:12:36 +01:00
Nghiathan13andClaude Opus 5.5 67d21b0a68 #3415: banded rows shade the same rows in Word and WordCraft
Word reads a table style without w:tblStyleRowBandSize / w:tblStyleColBandSize
as bands of zero and leaves its banded rows and columns unshaded (MS-OE376
§2.7.5.7, MS-OI29500 §17.7.6.5); ECMA-376 says one, which is what we did. Our
writer only wrote a size a style set, and our built-in styles set none, so a
table in Grid Table 4 saved by WordCraft had shaded rows here and none in Word.

- An unset or zero band size now means no bands, as in Word. PartSet::of_cell
  takes row and column band sizes; column bands get a size of their own
  (col_band_size).
- Our banded built-in styles and new table styles set both sizes to one, and
  the writer writes them (and zero), so Word bands them too.
- The reader reads both sizes, zero included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 18:21:49 +07:00
Marco Arnulfo 5124ee6d76 DOCX: hand Word's TOC control to the TOC when the field ends in a paragraph of its own (#2427)
Word lays out an automatic table as its control around a TOCHeading, the entries with the field
opening in the first, and a last paragraph holding only the field's end. That paragraph isn't a
TOC entry, so the TOC was kept in its control with the markers in the body; Update Table then left
the heading out of the TOC and wrote a second control around the entries alone. The reader checks
that TOC with the end moved into the last entry and hands it over as the range read, as before.
2026-10-21 11:25:28 +02:00
Marco Arnulfo 35fdb45156 DOCX: hand WordCraft's own TOC control back to the TOC, keep it when a block control covers the heading (#2427)
A TOC whose heading isn't a TOCHeading is written with an empty heading paragraph inside the
control. Keeping the control around it on reading left that paragraph non-empty, so each save and
Update Table added a blank line (toc_menu_offers_update_field_after_save_and_reopen, #2182). The
reader now compares against the TOC the writer would actually wrap (written_toc_span), which also
keeps the original control when another block control covers the TOC heading.
2026-10-21 10:59:52 +02:00
Marco Arnulfo 07a9880c14 DOCX: keep Word's Table of Contents content control around a TOC the writer doesn't rebuild (fixes #2427) 2026-10-21 10:24:17 +02:00
Fin Warman e8b41d157f M2.1: retain textbox parents and picture-fill fallback 2026-10-20 22:41:28 +01:00
NetterSilverandClaude Haiku 5.5 dc5e8cec41 Unstyle TOC entry links whose Hyperlink style has a localized id
Fixes #3239

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 09:59:41 +00:00
Dominik StrebelandClaude Opus 5.5 9c3d39c597 DOCX: Punjabi in Pakistan takes the Arabic-script theme font
lang_script mapped every `pa` tag to Gurmukhi. Measured in Word for Mac:
with w:themeFontLang w:bidi="pa-PK" a majorBidi run takes the theme's
Arab font, with "pa-IN" its Guru font. The test matrix gains both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 01:19:14 +02:00
Dominik Strebel 1fa2533a52 DOCX: majorBidi/minorBidi theme fonts follow the theme font language 2026-10-20 01:19:14 +02:00
Joey 156817d6ba Word signature lines stay signable after saving a .docx 2026-10-20 01:10:22 +02:00
Frédéric WeilandClaude Opus 5.5 912b98c833 Watermark: the text fills its shape's box, as Word's textpath does
Word's text watermark is a VML WordArt shape (`PowerPlusWaterMarkObject`)
whose `v:textpath fitshape` stretches the text over the shape's box (its
`style` width and height), centred on the margins and, when diagonal,
turned by `rotation:315`. WordCraft ignored the box: it drew the text
70 % of the page diagonal wide (half of it on screen), centred on the
page and along the page diagonal (55° on A4, not 45°). A "BROUILLON"
watermark in a 515 x 104 pt box came out about 1.4 times as large as in
Word.

- doc: `Watermark::size`, the shape's box (`None`: Word's default, half
  the page diagonal across, as high as the text's length makes it);
  `Watermark::frame` gives the box for a page, shared by the writer and
  the renderers.
- docx: the box is read from the shape's style and written back; a box
  equal to the default for the document's page reads as `None`, so a
  watermark made by Design > Watermark keeps following the page.
- pdf, render: the text's advance spans the box's width, its em the
  box's height, the em's foot (the font's win descent below the
  baseline) on the box's bottom; the box centred on the margins,
  turned 45° counterclockwise when diagonal.

Checked against Word for Microsoft 365 (ink bounds, pt, left top right
bottom; fonts here: Liberation Sans for Arial and Calibri, Liberation
Serif for Times New Roman):

  BROUILLON, Calibri, 515.4 x 104.1, 315°:
    Word 93.6 222.7 489.1 619.2; before 60.5 110.4 535.7 732.5;
    after 90.7 214.1 495.8 618.7
  SPECIMEN, Arial, 400 x 100, 315°:
    Word 136.3 261.6 455.0 580.3; before 66.7 108.5 539.0 727.7;
    after 137.8 255.4 454.6 573.1
  HXH, Arial, 300 x 100, flat: Word 158.9 378.2 436.3 449.8;
    before 105.1 353.8 490.1 489.6; after 158.9 380.2 435.8 449.3
  HXH, Times New Roman, 300 x 100, flat: Word 149.8 383.0 445.0 449.3;
    after 151.2 383.5 443.5 449.3
  COPIE, Calibri, 300 x 80, flat: Word 153.1 387.8 441.1 440.2;
    before 96.0 373.9 500.2 469.0; after 152.2 387.4 443.5 444.5

Tests: a watermark with a box of its own is written and read back with
it, Word's default box reads back as none; in the PDF, the text of a
300 x 80 pt watermark is 80 pt high, stretched across, starting at the
box's left edge on the margins' centre line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:31:10 +02:00
b8debb3050 Content controls: typed text takes the control's formatting, in any language (#3116)
* Content controls: typed text takes the control's formatting, in any language

Text typed in place of a content control's placeholder takes the
control's own run properties (w:sdtPr/w:rPr, ECMA-376 §17.5.2.27), as
in Word: in a protected form filled in Word for Mac, all 17 entered
values carry the style their control's rPr names. WordCraft kept that
rPr only as XML to write back and gave typed text the placeholder's
formatting minus the placeholder style.

It also knew the placeholder style only by its English id,
PlaceholderText. German Word writes Platzhaltertext (the name stays
"Placeholder Text"), so in forms from German Word every entry stayed
grey, on screen and in the saved file, and restoring a placeholder
added a second Placeholder Text style. The style is now found by its
built-in name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Content controls: typing drops a showing placeholder's own style

A control without run properties that shows its placeholder: text typed
over it drops the placeholder run's character style whatever its id
and name. Word localizes only the placeholder style's id, but another
program may localize its name too, and the name lookup then misses it.
Restoring a placeholder and new controls keep the name lookup.

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>
2026-10-19 12:53:44 -04:00
Daniele MoriandClaude Opus 5.5 53296879db Floating objects are drawn in their stacking order (relativeHeight)
Word stacks floating objects by wp:anchor/@relativeHeight, not by the
order of their anchors in the paragraph. The reader ignored the
attribute, so a panel anchored after the text boxes it frames, but
lower in the stack, was drawn over them and hid their text.

- wordcraft-doc: Float gets a `z` (0 = none, drawn in document order).
- wordcraft-docx: it is read from relativeHeight and written back as
  read; an object without one gets the next order above, as before.
- wordcraft-layout: place_objects collects each object's items with
  its order and emits them lowest first (stable, so objects without an
  order keep the paragraph's).

Four round-trip tests compare whole floats after a save; saving now
gives objects without an order one, so those tests compare without it
(`unstacked`). The order itself is covered by its own round-trip test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:44:38 +02:00
Fin Warman 328b36c9bd PR2453: merge main documentation 2026-10-19 07:28:58 +01:00
vangel76andClaude Sonnet 5.5 ffec4db77b cargo fmt after merging main
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 02:08:25 +02:00
vangel76andClaude Sonnet 5.5 3de7f8e31e Merge upstream/main into feat/radar-chart-1890
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-19 02:08:06 +02:00
PanyangYandBrandon Thomas de8a81c1e4 References: Word sources file import in Manage Sources (#2640) (#2897)
* References: Word sources file import in Manage Sources (#2640)

Manage Sources gains Import… and references.sources an "import": path
parameter: it reads a b:Sources document (Word's own Sources.xml) into the
document's sources with the same reader as a document's own bibliography
part. A tag already here is left alone, an untagged entry is matched by
title and author or gets a tag like a new source, the whole import is one
undo step, and a file that isn't b:Sources is refused. Desktop only; the
web build hides the button.

The docx reader now shares the b:Sources parser (crates/docx/src/
bibliography.rs) with the import, with the same dedupe and 10,000-source
cap; the hostile-input cap test still passes.

Tests: docx unit tests (names, kinds, accessed/DOI fallbacks, repeated
tags, empty entries, wrong root), engine import test (add, undo, second
import, untagged matching, refused file), ui tests (the dialog button asks
for a "sources" file; finish_pick imports), wordcraft-cli end to end
(import, cite, bibliography, save, reopen; the package's customXml
carries the sources). The button's label is in every interface catalogue.

ROADMAP progress log and docs/roadmap.md, gaps.md, file-format-parity.md
and target-app-parity.md get their rows.

* References: review fixes for the sources import (#2640)

- read_sources validates the root namespace instead of the local name alone:
  an un-namespaced or unrelated <Sources> is refused, and both bibliography
  namespaces read — OOXML's and Word's own 2004/10 (the VBA Source.XML
  format), the latter mapped onto the canonical b prefix with parse_ns.
  Tests: the 2004/10 namespace is accepted; a no-namespace or unrelated
  root is refused.
- The import loop builds the tag and (title, author-or-editor) sets once and
  keeps a per-stem counter, with the stem rule shared with auto_tag
  (auto_tag_base). Importing thousands of untagged sources no longer costs
  O(n^3). Test: untagged sources sharing a stem get Riv21, Riv211, Src, and
  a second import adds none.
- docs/target-app-parity.md and docs/file-format-parity.md no longer claim
  bibliography sources aren't saved in DOCX: #169 and #532 are closed and
  the round trip exists (#790).

* References: doc comments for the sources import helpers (#2640)

Hoist the untagged-source matching helper (by_author) to module level and
document sources, auto_tag_base and sources_ui, so the functions this PR
touches meet CodeRabbit's docstring-coverage pre-merge check.

* References: re-apply the sources import's progress notes (#2640)

The rebase onto current main took upstream's copies of ROADMAP.md,
docs/file-format-parity.md and docs/gaps.md, because the rows this PR
adds conflicted with upstream's own appends. Re-apply them on the current
text: the progress log and revision rows, the bibliography row's
source-format note, and #15's evidence, with each status line's change
note. The other two documents (docs/roadmap.md, docs/target-app-parity.md)
merged cleanly and keep their rows.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:54:47 -04:00
MarekSpodBiedryandBrandon Thomas ca6faa8c1d DOCX: Word 2007 WordArt (VML textpath) in the body is shown as text, not dropped (#2875) (#2903)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:52:01 -04:00
dec24458cf #1273: equations from Word files keep all their content (#2613)
- Math straight in the body or a cell (m:oMathPara/m:oMath without w:p,
  allowed by EG_BlockLevelElts) gets its own paragraph instead of vanishing.
- mc:AlternateContent Requires="m" picks the equation, not its fallback.
- Inside an equation, runs in w:hyperlink, w:fldSimple, w:dir, w:bdo and
  mc:AlternateContent are kept (EG_PContentMath); depth stays bounded.
- w:sym, w:noBreakHyphen and Symbol-font text in math runs become Unicode.
- Layout: n-ary operators, delimiters, accents and group characters the math
  font lacks use a fallback font instead of a missing-glyph box.
- Coverage test reads every ECMA-376 §22.1 element and checks no argument's
  text is dropped; tests for body-level math, Requires="m", symbols, hostile
  nesting and the font fallback. Parity docs and progress log updated.


Claude-Session: https://claude.ai/code/session_01P8veLveuUsJJm6UEcQPEuW

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:45:01 -04:00
beedd1b30d Drop Cap Options dialog (#2601) (#2602)
* #2601: Drop Cap Options dialog

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* gaps: keep the Drop Cap Options note on the dialog-depth row

* i18n: translate the two Drop Cap disabled reasons in the audited catalogs

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:44:30 -04:00
5fdd0a0e89 Watermarks in header table cells drawn behind the text (#1775) (#2580)
A floating picture or VML shape in a header table cell is placed on the page behind the text.

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:43:55 -04:00
divyanshu2074andBrandon Thomas c9468a13cb fix(docx): support self-closing w:compat in settings merge (#2131)
* fix(docx): support self-closing w:compat in settings merge

* Drop the duplicate divyanshu2074 entry in people.toml (already on main)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 11:35:43 -04:00