The Info page edited a copy of the document properties that was re-read
from the document every frame and only written back when a field lost
focus, so each keystroke vanished on the next frame and nothing was ever
committed. The fields now commit through file.properties as you type
(undoable, one step per visit to a field, marks the document dirty).
The web build's Save writes the bytes itself instead of running
file.save, so it never stamped the modified time, last-modified-by and
revision. Both paths now share Session::stamp_save.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
file.setAuthor now documents {"name": string} in the command list, trims the
name, strips control characters, caps it at 255 characters and refuses a blank
name. wordcraft-cli run and headless mcp take --author NAME. review.trackChanges
lists {"value"?: bool} so scripts find the explicit, idempotent form.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Zotero 1: range fields and custom document properties round-trip
Citation managers' ADDIN fields (Zotero, Mendeley, EndNote) are now kept as
range fields: FieldStart/FieldEnd markers with the result as ordinary,
formatted content between them, possibly spanning paragraphs (a Zotero
bibliography). Before, the result was flattened to plain text and a
multi-paragraph field lost its end.
- doc: InlineObject::FieldStart/FieldEnd, Document::field_ranges,
balance_field_ranges; CustomProp and custom_props with get/set/remove.
- docx: read ADDIN fields as ranges (field codes up to 2 MiB), write them
back as begin/instr/separate…end; orphan markers are dropped on save so
files always hold whole fields. docProps/custom.xml is parsed and
regenerated (simple types as text, vectors/blobs verbatim) instead of
passed through, so Zotero's ZOTERO_PREF_n settings are editable.
- engine: file.properties reads and sets custom properties.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero 2: wire-protocol client and document bridge
New crate wordcraft-zotero (L5) speaks Zotero's published word-processor
protocol (port 23116; 32-bit transaction id + big-endian length frames,
JSON payloads, ERR: replies):
- client: sends an integration command (addEditCitation,
addEditBibliography, addNote, refresh, removeCodes, setDocPrefs) and
answers Zotero's calls until Document_complete or the connection
closes (cancelled dialog). Answers go through a callback so the app can
apply them on its UI thread; run_on_session answers directly.
- Bridge: implements Zotero's Document_*/Field_* calls on a Session.
Fields are stored as Word's plugin stores them (ADDIN ZOTERO_ range
fields; prefs in ZOTERO_PREF_n, with Zotero's ReferenceMark field type
kept as Word's Field in the file), so documents stay Word-compatible.
Rich text (RTF or HTML) keeps citation formatting; bibliographies get
a Bibliography style from setBibliographyStyle; note styles create
footnotes/endnotes, and deleting a lone note citation removes its note;
placeholder links convert to fields. One transaction = one undo step.
Field ids stay stable through inserts and deletes.
- Tests: a fake Zotero over a real socket, every call, docx round trip,
hostile arguments for every method, random frames (proptest).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero 3: Zotero in the app and the CLI, tested with the real Zotero
- ui.zotero.<command> (addEditCitation, addEditBibliography, addNote,
refresh, removeCodes, setDocPrefs), ui.zotero.status and
ui.zotero.answer. The socket runs on a background thread; each of
Zotero's calls is answered on the UI thread against the session, and
Zotero's questions become a dialog the session waits on. Web builds
report that Zotero needs the desktop app.
- wordcraft-cli zotero <command> <file> [--save] [--trace] [--port].
- Bridge: a selection Zotero makes to show a citation is put back when
the session ends.
- ui_shot: {"wait": ms} keeps frames running for background work.
- docs/zotero.md.
Checked against Zotero 10.0.6: refresh of a citation and bibliography
(APA), its "modified citation" question answered in the app's dialog,
RTF results with italics, Bibliography style, prefs round trip.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero 4: switching between in-text and note styles moves citations
Document_convert and Field_convert now move a citation between the
text, footnotes and endnotes, as Zotero asks when the document's style
changes between an in-text and a note style: into a new note at the
same place, or back into the text where the note mark was (removing a
note that held only the citation; keeping one with other text). Field
ids survive the move, formatting is kept, and each Zotero session stays
one undo step.
Checked against Zotero 10.0.6: unlink citations (removeCodes, with its
confirmation question) and a refresh after switching the document to
Chicago notes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero 5: Zotero tab in the ribbon
A Zotero tab (before Help) with Add/Edit Citation, Add Note, Add/Edit
Bibliography, Refresh, Document Preferences and Unlink Citations. The
buttons are disabled while a Zotero command is running and in the web
build. Three new icons drawn in code (document preferences, unlink,
add note); the others reuse the References tab's. Labels and status
messages translated (zh-hans, zh-hant, ja).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero: Add/Edit Bibliography inserted nothing
Zotero reads a newly inserted bibliography field back before filling
it, and unlinks a field whose text is empty, taking it for a
bibliography the user deleted. Our new fields were empty, so Add/Edit
Bibliography ended with Field_removeCode and nothing in the document.
New fields now hold "{Citation}" until Zotero writes the result.
Found by tracing the session with Zotero 10.0.6; regression test added.
wordcraft-cli zotero takes --cmd (run before the Zotero command, e.g.
caret.docEnd) to reproduce sessions from a given caret position.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Zotero: log each command's outcome and Zotero's calls to the app log
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Cargo.lock: wordcraft-zotero at the workspace version (0.4.0)
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>
* 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>