Commit Graph
35 Commits
Author SHA1 Message Date
Hamza Naveed cbe6a9d5f7 Pin that a comment id past u32 is refused, not truncated (#3677) (#5273)
* Pin that a comment id past u32 is refused, not truncated (#3677)

`validate_comment_id` refuses an `id` that does not fit a `u32`, so the `as
u32` casts behind it can no longer reach another comment -- but nothing pinned
it, and the casts are still there to be reached if the guard moves.

The ids the test sends are the ones that would alias onto real comments if the
truncation came back: 2^32 truncates to 0 (and the first comment is 0), 2^32 +
id to that comment, 4294967301 to 5 as the report describes, and u64::MAX to
u32::MAX. Each of the four commands that take an id is tried with each, then
the comments are checked to be unchanged and unresolved, and a valid id is
accepted afterwards so the guard is shown to be a range check rather than a
refusal.

* Comment ids: the values past u32 go in the existing invalid-id test, refused by the range check
2026-11-05 01:32:15 +01:00
mmazzieriandClaude Opus 5.5 75fc50fb77 DOCX: picture brightness, contrast and transparency (VML washout, a:lum, a:alphaModFix) (#1961)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 21:47:44 +01:00
FredApps 8218dea5bd Fix #5559 follow-ups: generated list prefix, hidden-text objects, turned clipped lines, kept-markup checks once per export
- update_generated keeps what precedes a wrapped list's field (r4468529243)
- from_doc keeps objects in hidden text when hidden text is written (r4469018820)
- clipped cell lines in a turned frame are drawn once, clipped, turned (r4469018900)
- kept::while_writing works out reply map and page fingerprint once per export (r4468529227)
- odt_charts fixture declares its namespaces (r4468529259)
- guard test for character styles based on a missing parent (r4469018843, not a bug)
2026-11-04 17:43:46 +01:00
Claude 078945e10c Emphasis marks: modelled, drawn, set, and kept in DOCX and ODT
Word's Font › Emphasis mark (a mark over or under each character, used
to stress East Asian text) wasn't modelled. DOCX `w:em` and ODT
`style:text-emphasize` were dropped.
- Model: `CharProps::emphasis` (dot, comma, circle, dot below, or none
  to turn off a style's).
- Layout: a mark is drawn over (or under) each character, and the line
  is made taller to fit it.
- `format.emphasisMark {"mark": …}` sets it, and Format state reports
  it. The catalog lists it under Home › Font.
- DOCX: `w:em` is read, and written in its place in `w:rPr` (after
  `w:cs`, before `w:lang`).
- ODT: `style:text-emphasize` maps to it: an accent is the comma, a
  disc the dot, a dot below the under-dot.

Also fixes two problems from the previous commit (ODT variables):
- Field updates blanked a SET field's stored result. The layout
  already draws nothing for SET, so its result stays as it was, as
  `ask_and_set_fields_draw_nothing` pins.
- A clippy lint (manual char comparison) failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 09:46:23 +00:00
Claude 56188b97f4 Gap 19: ODT variables as SET/REF fields; SET evaluated; character styles keep what they turn off
- ODT variables (`text:variable-set`, `text:variable-get`, §7.4) were
  read as their text. A set is now Word's SET field, followed by a REF
  when it is displayed; a get is a REF. They are written back declared
  (`text:variable-decls`), the sets hidden with a get after them.
- The engine now evaluates SET (ECMA-376 §17.16.5.58). It shows
  nothing and gives its name a value. REF to a name no bookmark has
  shows that value; a REF before the SET shows the last value set, as
  Word's bookmark does after an update. This works for Word documents
  too.
- Custom character styles now keep what they turn off under a parent
  that turns it on (`fo:font-weight="normal"`…), as paragraph styles
  already do.

A LibreOffice document with variables round-trips with the same text,
and the saved file validates against the schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 08:37:37 +00:00
Claude 7848757954 Gap 19: ODT hidden form controls kept; .ott templates opened and saved
- Hidden form controls (`form:hidden`: a name and value the form sends,
  not shown) were dropped. They are now kept verbatim, with their
  properties, and written back into the form with ids of their own.
- OpenDocument text templates (`.ott`) weren't opened. They now open
  as documents, and save as templates: the template's media type goes
  in `mimetype` and in the manifest's root entry (ODF 1.3 Part 2 §3.3).
  This works with a password too. `.ott` is listed wherever `.odt` is:
  open and save formats, file dialogs, export, the web app and its
  media type.

A LibreOffice template round-trips with the same text. 300 fuzzed
documents exercising custom shapes, cell formulas, kept parts, turned
text and forms caused no crashes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-02 05:49:16 +00:00
Claude 2036ad2e99 Gap 19: ODT table cell formulas as Word formula fields
ODT cell formulas (`table:formula`) were lost, and only their value was
kept as text.
- Reading: LibreOffice's formulas, in its own syntax
  (`ooow:sum <A1:A3>`, `<B1>*<B2>+<B3>`, `mean`, `min`, `max`,
  `product`, `abs`, `sign`, `count`, `add`/`sub`/`mul`/`div`/`pow`, lists
  joined by `|`), open as Word formula fields (`=SUM(A1:A3)`) showing
  the cell's value. Update Field recalculates them.
- Writing: formula fields in cells are written back as cell formulas
  with their value. ABOVE, LEFT, BELOW and RIGHT become the cells they
  name.
- Cells holding a plain number are written as numbers, so LibreOffice
  calculates with them. The `ooow` prefix is declared; without it,
  LibreOffice reports "Expression is faulty".

A formula one side can't say keeps its value as text. A LibreOffice
table of sums round-trips with the same text, and the saved file
validates against the schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 22:57:17 +00:00
Claude 2877514d9e Gap 19: ODT bibliography marks as citations of sources
ODT citations (`text:bibliography-mark`) were read as their text, and
the sources they carry were lost. So an ODT bibliography had nothing
to update from.
- Reading: each mark is now a `CITATION` field of its source, which
  becomes one of the document's bibliography sources (the first mark
  of an identifier gives it). The identifier is the tag, the type the
  kind, plus author, title, year, publisher, address, journal, volume,
  pages, URL, editor, edition, number and month.
- Writing: a `CITATION` of a known source is written back as a mark
  carrying the source.

References › Update rebuilds an ODT bibliography from them. A
LibreOffice citation and bibliography round-trip pixel-identical and
validate against the schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 22:13:47 +00:00
Claude 0b6b88f414 Gap 19: ODT custom document properties and user fields as DOCPROPERTY
ODT custom document properties (`meta:user-defined` in `meta.xml`) were
dropped, and the fields showing them or a user field were read as
plain text.
- Properties: `meta:user-defined` values (string, number, true/false,
  date) are now WordCraft's custom document properties, and so are
  user field declarations (`text:user-field-decl`).
- Fields: `text:user-defined` and `text:user-field-get` are Word's
  `DOCPROPERTY` fields, which WordCraft updates from those properties.
- Writing: custom properties go back into `meta.xml`, and a
  `DOCPROPERTY` naming one is written as `text:user-defined`.

A LibreOffice document with a custom property and a user field
round-trips pixel-identical, valid against the schema. Its user field
comes back as a document property.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 20:30:48 +00:00
Claude 72e9593de9 Gap 19: ODT index marks as XE fields; updating an index held around its entries
ODT index marks (`text:alphabetical-index-mark`) were dropped. So
References › Update Index on an opened ODT index found no entries and
left the old list in place. Now:
- a mark at a point is Word's `XE` field, its `text:key1`/`text:key2`
  the levels above its entry;
- a mark over text (`-mark-start` … `-mark-end`, in one paragraph) is
  an `XE` field whose entry is the marked text, which stays text;
- `XE` fields are written back as point marks.

Engine: Update rebuilt only lists in WordCraft's own form, a paragraph
holding the field. A list held as a field around its entries, as ODT
and Word files hold an index, bibliography or table of figures, is now
first made the field's own paragraph, then rebuilt. That happens only
when the field starts its paragraph, so no document text is replaced.

A LibreOffice document with an index mark round-trips pixel-identical,
the mark kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 19:04:44 +00:00
Claude 7eee06a1ab Gap 19: ODT form controls opened and saved as content controls
ODT form controls (ODF 1.3 §13: controls in `office:forms`, each
placed in the text by a `draw:control`) were dropped on open, values
and all. They now open as WordCraft content controls, each with its
value, entries and selection; its name becomes the tag and its label
the title:
- text fields and text areas: plain text controls;
- check boxes: check boxes;
- radio buttons: check boxes with round symbols;
- list boxes: drop-down lists;
- combo boxes: combo boxes;
- date fields: date controls.
Saved as DOCX they become Word content controls.

Content controls of those kinds in the body are written back as form
controls with a `draw:control` anchor; in headers and footers they stay
their text. LibreOffice shows the round-tripped controls with their
values, and the content validates against the ODF 1.3 schema.

Not read: buttons, images, hidden and other controls. Controls' sizes
and looks aren't kept.

Flow model: new `Inline::Control`, kept only by flows that ask for
boxes (ODT); other formats get its text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 14:46:01 +00:00
Claude b36e0b9fe9 Gap 19: password-protected ODT opened and saved
A password-protected ODT (ODF 1.3 Part 2 §3.4) failed to open. The
package is now decrypted with the password into a plain one, which the
reader opens as before.
- Supported: AES-256-CBC with a SHA-256 or SHA-1 start key, PBKDF2
  with HMAC-SHA-1, and SHA-256 or SHA-1 1K checksums (LibreOffice's
  encryption).
- Blowfish (the old default) is refused with a message naming it.
- A missing or wrong password gives the same errors as Word files, so
  the app asks for the password.
- Hostile parameters are bounded: iteration count, IV size, ragged
  data, inflated size.

Saving with a password (and saving a document opened with one) writes
the ODT encrypted as LibreOffice does. Every file but the `mimetype`
and manifest is deflated and encrypted, each with its own salt and IV,
and the manifest validates against the ODF manifest schema.
LibreOffice opens WordCraft's encrypted files with the password and
refuses a wrong one; WordCraft opens LibreOffice's.

No new dependencies: the AES, SHA, HMAC, miniz and getrandom crates
the workspace already uses for Word encryption.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 14:24:03 +00:00
Claude 275037932f Gap 19: ODT embedded spreadsheets and OLE objects kept whole
LibreOffice writes an embedded object's replacement picture in its own
metafile format, which WordCraft can't decode. So an embedded
spreadsheet (`draw:object`, ODF 1.3 §10.4.6) or OLE object
(`draw:object-ole`, §10.4.7) vanished on open, and saving lost it.

Such objects are now kept whole: their files, media type (from the
manifest or the sub-document's body) and replacement picture.
- Display: the replacement picture when WordCraft can show it, else a
  placeholder of the object's size drawn in code (ruled for a
  spreadsheet).
- ODT save: written back as the same object, with its manifest entries
  and original replacement. LibreOffice renders a round-tripped
  spreadsheet pixel-identically.
- Document model: the files live in `passthrough` under `odf-object/`,
  linked from the picture's `ole` by a marker. The DOCX writer skips
  the marker and writes the picture alone.

Flat ODT, a fix to the chart commit: inline sub-documents lacked the
required `office:mimetype`, so the schema rejected flat files holding
charts. Sub-documents now go inline as full `office:document`s (media
type, styles, settings) and are split back into their files on
opening. Replacement pictures and OLE files go inline as base64. A
picture's data is matched to the element that holds it, no longer to
the last `draw:image` before it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 13:35:59 +00:00
Claude 135ce2229d Gap 19: ODT charts in drawing groups drawn on open
Review of the chart commit: the engine drew opened charts only at the
paragraph level, so a chart that was a member of a `draw:g` group
opened blank. The pass now goes into groups (bounded depth).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 12:49:08 +00:00
Claude 3cc5185b1e Gap 19: ODT charts opened and saved as editable charts
A chart object (`draw:object` whose sub-document is an `office:chart`,
ODF 1.3 §10.4.3, Part 3 §11) used to open as its replacement picture.
It is now read into WordCraft's chart model: column, bar (stacked or
not), line, area, pie, doughnut and scatter charts, with categories,
series names and values (gaps included) taken from the chart's own
table through the series' cell range addresses, plus title, legend
position and data labels. The engine draws opened charts in the
document's theme colours, and Chart Design's commands edit them.
Charts WordCraft can't edit as they are (3D, percent-stacked, mixed
plot types, a second value axis, radar and the like) keep their
picture.

WordCraft charts are written as ODF chart objects shaped like
LibreOffice's: a data table, each series and slice in its accent
colour, pies clockwise from the top. They validate against the ODF 1.3
schema, and LibreOffice draws them like WordCraft does. In flat
documents they are written inline as an `office:document`.

Bug fixed: flat ODT reading ended the body at the first nested
`</office:body>`, so an inline sub-document dropped the text after it.
Element lookup now follows nesting, with a bounded depth.

Flow model: new `Inline::Chart`, kept only by flows that ask for
boxes (ODT); other formats still get the alt text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 12:43:51 +00:00
Claude 694ef8da96 Gap 19: flat OpenDocument text (.fodt) opened and saved
A flat ODT holds the whole document in one `office:document` element
(ODF 1.3 Part 3 §2.1.2), its pictures inline as base64
`office:binary-data` and its formulas inline in their `draw:object`.
It is opened by taking it apart into a package's files (content,
styles, meta, settings; pictures and formulas as entries, each part's
root keeping the document's namespaces) and reading that package, and
saved by putting a package's files together: one set of automatic
styles, every part's namespaces declared once on the root, pictures and
formulas inline without the link attributes.

`.fodt` opens and saves from Open, Save As, the CLI and the web app.
LibreOffice's own flat documents round-trip as their packages do, and
the written files validate against the ODF 1.3 schema.

The parity doc's ODT fidelity is re-estimated at ~80%, and its status
line, which hadn't been updated since the Asian fonts change (a text
replacement that matched nothing), now lists every change since.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 10:05:45 +00:00
Claude 5374375301 Gap 19: ODT date formats, sequences, chapters and references
Fields that were read as plain text now open as Word's, so Update Field
recomputes them, and are written back as ODF's:
- dates and times in their date or time style (`number:date-style`,
  `number:time-style`) ↔ `DATE`/`TIME \@ "picture"`: day, month (by
  number or name), year, weekday, hours (12-hour with AM/PM), minutes,
  seconds and the text between, both ways; data styles are written as
  automatic styles and the `number` namespace declared;
- sequences numbering figures and tables (`text:sequence`) ↔ `SEQ` with
  its number format; on save the sequences are declared
  (`text:sequence-decls`); on open a bookmark named by the sequence's
  reference name covers the caption's label and number ("Figure 1"),
  what references to it show;
- chapter fields (`text:chapter`) ↔ `STYLEREF "Heading n"`, by name or
  number (`\n`);
- reference marks as bookmarks, and references to them and to sequences
  (`text:reference-ref`, `text:sequence-ref`) as `REF`/`PAGEREF`.

Captions renumber, references to them and chapter fields refresh after
Update Field (engine test `odt_fields.rs`). A LibreOffice document with
all of them round-trips 13 px apart with the same text; the written
content and styles validate against the ODF 1.3 schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 09:12:52 +00:00
Claude bb5dc4bcaa Gap 19: ODT bookmarks keep the text they cover
A bookmark over text (`text:bookmark-start`…`text:bookmark-end`, in one
paragraph or across several) was read as a point at its start, and a
Word bookmark over text was written as a point: a reference to it then
updated to nothing. The flow model gets the end of a bookmark
(`Inline::AnchorEnd`, in flows asked for fields, as ODT's is), the
document gets the bookmark's start and end where they are, and the
writer writes a start and an end (a point, `text:bookmark`, when it
ends where it starts). A bookmark started twice, ended twice or ended
before it starts is kept once, as a point when it has no end after it.

References to bookmarks, LibreOffice's references to headings included,
now update to the bookmarked text (engine test `odt_fields.rs`, through
a save and reopen).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 08:53:42 +00:00
Claude aea3b4c7d7 Gap 19: ODT tables of contents and indexes read and written
Tables of contents, tables of figures (`text:illustration-index`,
`text:table-index`), alphabetical indexes and bibliographies open as
WordCraft's: Word's `TOC` (levels from the source's
`text:outline-level`; `\c` and the caption name for figures), `INDEX`
and `BIBLIOGRAPHY` fields over their entries as last generated. With a
title the field goes in the title paragraph, as References › Table of
Contents inserts one; without, it spans the entries (new flow inlines
`FieldStart`/`FieldEnd`, mirroring the document's range fields). Update
Table then regenerates them from the headings (engine test
`odt_toc.rs`, titled and untitled, through a save and reopen).

LibreOffice's Contents 1–10, Contents Heading and Figure Index 1 styles
are WordCraft's TOC 1–9, TOC Heading and Table of Figures, both ways,
also when a file names them without defining them. WordCraft's tables
are written as ODF indexes with their source and entry templates, so
LibreOffice updates them, the entries' links in the Index Link style.
User and object indexes are read as their paragraphs; index marks are
not read yet.

Found on the way, both shown by the table of contents' round trip:
- a style based on LibreOffice's generic Heading style (as its headings
  and index headings are) lost that parent on save, and with it its
  font and spacing;
- a paragraph's tab stops were measured from its own indent only, not
  from an indent its style gives (LibreOffice's `TabsRelativeToIndent`),
  and a named style's stops not from its indent at all.

A LibreOffice document with a table of contents, a table of figures and
an alphabetical index round-trips pixel for pixel (it was 29,717 px
apart and a page short). The parity doc's ODT fidelity is re-estimated
at ~72%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBVQLyDnrG8Tn1yDQcUabC
2026-11-01 08:39:20 +00:00
Dylan M. Taylor a2435388e5 Refactor engine for strict Clippy checks 2026-10-28 01:06:48 +00:00
MexandClaude Opus 5.5 40a7fe6be2 Make three-column header/footer presets editable tables (#2197) (#2266)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:34:23 -04:00
aiongg c795e3cd41 CI: one test binary per crate, nextest, line tables only (#4025)
* CI: line-table debug info for dev builds, none for dependencies

* CI: one integration-test binary per crate

* CI: nextest with doctests, test build without examples, one wasm check

* CI: the interface audit reports missing catalog entries instead of failing

* Docs: the interface audit, not CI as a whole, passes with missing catalog entries
2026-10-23 14:24:37 -04:00
aiongg 5dc68481ed Integration: track t09
crates/ui-egui/src/backstage.rs: t03's bidi_display on the Recent names over t09's borrowed name/path. people.toml: union. Tests: both tracks' tests kept whole. Docs: status lines combined, rows unioned or word-merged.
2026-10-22 01:48:25 +00:00
aiongg b0fb2b9386 Merge PR #2101: File: recovery copies and .json files store pictures as base64 (#586 follow-up)
ROADMAP.md: status line, progress log and revision history carry both sides.
docs/file-format-parity.md: status line and change log carry both sides.
2026-10-22 01:22:47 +00:00
aiongg 2d4455ff34 Integration: #1847 reject comment IDs above u32::MAX 2026-10-22 01:19:54 +00:00
Mex bf9571d7e1 Merge branch 'main' into fix/reject-invalid-comment-ids 2026-10-20 22:41:14 +02:00
NetterSilverandClaude Haiku 5.5 5a82eafa5a Refresh DATE/TIME fields when opening .doc/.dot by extension (#2418)
Found while testing crates/engine/src/io.rs:183-187 (read_bytes, the "doc" | "dot" arm returns before opened() is called)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-18 04:55:00 -04:00
Mex 62bb20187d review (#1690): reject malformed explicit comment IDs 2026-10-15 21:48:27 +02:00
asystemoffieldsandClaude Opus 5.5 5795259c45 File: recovery copies and .json files store pictures as base64 (#586 follow-up)
Native JSON wrote Document::media and Document::passthrough with serde's
default for bytes, an array of numbers. With a synthetic 27 MiB picture a
recovery copy (compact) was 96.4 MiB and a .json file (pretty-printed,
one number per line) 285.5 MiB.

Each part is now one standard base64 string (doc::byte_store, using the
workspace base64 crate), encoded straight into the writer. Reading accepts
base64 or the old array of numbers, so recovery copies and .json files
written before still open; anything else is an error, never a panic.

Measured (release, same synthetic picture): compact 96.4 -> 36.0 MiB,
pretty 285.5 -> 36.1 MiB, write 252/614 -> 52/57 ms, read 404/533 ->
46/37 ms. A recovery snapshot of a 6,000-paragraph document with the
picture: 99 -> 39 MB, worker time 2.3 -> 0.26 s, start-up scan 1.03 ->
0.14 s.

Tests: byte_store unit tests (round trip, old arrays, hostile values);
recovery_format.rs native_json_stores_bytes_as_base64,
native_json_with_byte_arrays_still_opens, native_json_with_bad_bytes_is_an_error,
ignored measurement native_json_size_with_a_large_picture; desktop.rs
copies_with_byte_arrays_written_before_still_restore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 10:10:23 -04:00
Hamza NaveedandBrandon Thomas ffacc4c08c Fields: refresh unlocked DATE and TIME when a document is opened (#1679)
A `DATE` or `TIME` field showed whatever was cached the last time the file was
saved, so a letter written from a template went out carrying the date the
template was saved. Word refreshes those two whenever it shows, prints or
exports a document; WordCraft only did so on an explicit Update Field(s), on
insert, or when printing with "Update fields before printing" on. Headless
`convert` to PDF was the worst of it: nothing hints the date is old.

Refresh them where a document is read instead, in a new `io::opened` beside
`ensure_nonempty` -- the two things a freshly-read document needs -- so `text`,
PDF, rendering and the window all show the same, current value. The model's
stored result is the one source they share; patching each output path would
have needed a clock in two places and still missed the next one.

Narrow on purpose. Only `DATE` and `TIME`: `CREATEDATE`, `SAVEDATE` and
`PRINTDATE` record when something happened and are not "now", and `REF`, `TOC`
and citations stay on explicit update, as in Word. A field carrying `w:fldLock`
keeps its stored result, which is what the lock is for. Headers, footers and
text boxes are covered too, not only the body.

The field code is untouched, so saving writes the field back as it was, with a
current cached result -- as Word writes one.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 01:07:07 -04:00
3d8f8144d9 File: recover unsaved work after a crash (#518) (#586)
* 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>
2026-10-14 17:21:57 -04:00
b1e8ae07e7 ODT import: keep each section’s page properties (#1379) (#1506)
* #1379: WIP

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

* #1379: master pages on table styles start sections too; spec tidy-ups

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>
2026-10-14 14:06:49 -04:00
1f2b2287b6 RTF import: keep section breaks and their page settings (#1378) (#1495)
* #1378: WIP

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

* #1378: pasted sections link their headers; section orientation wins over the document's

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

* #1378: Text from File clears inserted sections' headers too

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>
2026-10-14 13:44:02 -04:00
ReesandBrandon Thomas 1f09547f4e SCRUM-34: protect original files during native saves (#621)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 02:11:13 -04:00
AiBizzAppsandClaude Opus 5.5 a45c8a41de DOCX: keep a .docm's macros and signatures on save; right content types for .docm/.dotx/.dotm (#172)
* DOCX: keep a .docm's macros on save; write .docm/.dotx/.dotm content types

Saving a .docm deleted its VBA project and wrote the plain-docx main
content type under the .docm name, which Word rejects; .dotx was written
as a document, and .dotm could be opened but not saved. io_ext sent every
Word extension to wordcraft_docx::write, which always emitted a .docx
package, and the reader never kept the VBA parts.

- wordcraft_docx::Flavor (docx/docm/dotx/dotm) picks the main part's
  content type; write_as(doc, flavor) writes it, write() stays .docx.
- The reader keeps word/vbaProject.bin (and the vbaData.xml it relates
  to) as opaque bytes in Document::passthrough, the mechanism custom.xml
  already uses. Macro content is never parsed or run. The writer emits
  them, with their relationships and content types, only for .docm/.dotm;
  a .docx/.dotx leaves them out cleanly (as Word does) and the open
  document still holds them for a later Save As .docm.
- Empty projects, missing or malformed VBA data, duplicate or odd
  relationships never panic and never stop the document from opening;
  part sizes stay under the existing package limits.
- .dotm joins the open/save lists, and saving to .docm/.dotx/.dotm binds
  the session to that file like .docx.

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

* DOCX: carry vbaData.xml as opaque bytes, never parse it

The first cut gated word/vbaData.xml through xml::parse on read and on
write. That parser rejects UTF-16 (a valid XML encoding), so a
UTF-16 vbaData.xml was silently dropped and the saved .docm kept
vbaProject.bin without its VBA data; and it is lenient (no end-name
check, auto-closes at EOF), so malformed data passed the gate anyway.
WordCraft is a passthrough for macro parts: it should emit exactly what
the user's file had. Now vbaProject.bin and vbaData.xml are both carried
byte for byte (bounded by the reader's existing size limits); only the
relationship part is read, to find the VBA data. A relationship to a
missing VBA data part keeps the project and writes no dangling link.

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

* DOCX: read UTF-16 XML parts instead of dropping them

xml::parse rejected any part with a UTF-16 BOM, and Package::rels
swallowed that error as "no relationships". A UTF-16
word/_rels/vbaProject.bin.rels (valid per OPC) therefore hid
vbaData.xml, and saving the .docm dropped it and its relationship.
parse now transcodes UTF-16 LE/BE with a BOM to UTF-8 (lossy, bounded
at 1.5x, never panics on odd lengths or unpaired surrogates), so every
part benefits. Unreadable relationship parts are now logged rather than
silently treated as empty; the macro project is still kept.

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

* Web Save keeps the opened Word format (.docm stays .docm)

Browser Save (file.save without a path) always downloaded
<stem>.docx, so an opened .docm/.dotm was written as a plain .docx and
lost its macros on an ordinary Save. Save and the Save As suggestion
now default to the opened file's extension when it is a Word package
(.docx/.docm/.dotx/.dotm, via engine io::is_word_package), else .docx.
Macros are only dropped on an explicit conversion to a macro-free name.

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

* DOCX: carry every part a VBA project relates to, so signatures survive

A signed .docm/.dotm lost its VBA signature on save: the reader kept
only vbaProject.bin and vbaData.xml, and the writer rebuilt
vbaProject.bin.rels with the wordVbaData relationship alone. Under a
"signed macros only" policy the macros then stop running, although the
project bytes (which the signature covers) were unchanged.

Generalised instead of special-casing signature types: the reader now
keeps every internal part vbaProject.bin.rels points to (VBA data, the
legacy/Agile/V3 signatures, anything else) as opaque bytes, with its
relationship type and its content type from [Content_Types].xml, in a
small manifest in Document::passthrough. .docm/.dotm re-emit them all;
.docx/.dotx still drop the whole VBA set. Bounded at 64 parts; targets
outside word/, relationship parts, the project itself, missing parts
and parts the writer produces itself are skipped, never shadowed.

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

* DOCX: match VBA part names case-insensitively; keep colon names relative

Two edge cases in carrying a VBA project's related parts:
- The reader's word/ prefix check was case-sensitive, so a relationship
  to /WORD/vbaData.xml (the same part under OPC's case-insensitive part
  names, and found by Package::get) was skipped and lost on save. The
  directory is now matched case-insensitively and normalised to word/,
  the name the writer uses.
- Stripping word/ from /word/sig:legacy.bin gave Target="sig:legacy.bin",
  which parses as a URI scheme rather than a package part. A target
  whose first segment has a colon is now written as "./sig:legacy.bin";
  ordinary names keep the plain form Word already accepts.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:34:52 -04:00