Commit Graph

22 Commits

Author SHA1 Message Date
Valentin Klein 7dcfa07c15
File dialogs (Open, Save As, Export) no longer freeze the window on Linux (#94) (#246)
* Tests: shapes and pictures can be clicked, resized, dragged and nudged

Fixed by #46 but only text boxes were covered. These walk the editor's
path for shapes (rectangle, ellipse, star, arrow) and pictures: hit test,
select, resize with arrange.bounds, drag to float with Square wrap,
nudge, undo. Covers #142 and #82.

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

* Font list offers every weight a family installs, named as Word names them (#219)

Fonts were listed and matched only by their typographic family name
(name ID 16, e.g. "Roboto"), so a face other than Regular, Bold or Italic
(Light, Medium, SemiBold, Black, Condensed...) never appeared in the font
list, and a document asking for "Roboto Light" fell back to a substitute.

Each face now also carries its legacy family name, the one Word lists and
stores in documents: name ID 1 for static fonts ("Source Sans 3
Semibold"), or the family plus the style words other than Regular, Bold
and Italic for a variable font's named instances ("Inter Light"). The
font list includes these names, has_family and face() resolve them (bold
and italic still choose or synthesise within the faces the name covers),
and asking for a legacy name loads the whole installed family. The
system font scan now also reads fvar, so a variable font's instances are
listed before it is loaded.

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

* Absolutely positioned VML text boxes float instead of sitting inline (#149)

A legacy VML shape (w:pict > v:shape / v:rect) with position:absolute in its
style was read as an inline object, so a tall text box in a header made the
header as tall as the box and pushed the body down the page.

The reader now maps the VML style to the same float model DrawingML anchors
use: left/margin-left and top/margin-top offsets, mso-position-*-relative
reference areas, mso-position-horizontal/vertical alignment, mso-wrap-distance-*
and w10:wrap (square, tight, through, topAndBottom; none or absent is in front
of the text, or behind it at a negative z-index). Shapes without
position:absolute stay inline. Saving writes the boxes as DrawingML anchors
with the same placement.

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

* Reject All removes paragraph breaks inserted with Track Changes on (#229)

Enter under Track Changes already recorded the new paragraph mark as an
insertion, but accepting or rejecting only cleared that flag, so Reject All
left the paragraphs split. Rejecting an inserted mark (or accepting a deleted
one) now joins the paragraph with the next one again; Accept All, Reject All,
Accept/Reject at the caret or over a selection, Next/Previous Change and the
Reviewing Pane all see paragraph marks.

Backspace at a paragraph start and Delete at a paragraph end now record a
tracked deletion of the mark (or remove a mark the same author inserted)
instead of only moving the caret; tracked deletions across paragraphs mark
the paragraph marks too.

DOCX reads and writes paragraph-mark revisions (w:ins / w:del in the
mark's w:rPr), and a tracked mark is drawn in its author's colour when
formatting marks are shown.

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

* Table Layout: Text Direction turns cell text (#226)

The Text Direction button only flipped a flag that layout and rendering
ignored (and docx always wrote it as btLr), so pressing it did nothing
visible.

Cells now carry a text direction (horizontal, top-to-bottom tbRl,
bottom-to-top btLr). The button cycles through them like Word's, for every
selected cell. Layout runs turned text along the cell's height: an exact
row height sets the line length, otherwise the row grows to fit the
unwrapped text. Turned lines are drawn in a turned frame on screen and in
PDF, and clicks, caret, and selection follow the turn. docx reads and
writes w:textDirection with the matching value.

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

* File dialogs no longer freeze the window on Linux (#94)

Open, Save As, Export and Insert/Change Picture used rfd's blocking
FileDialog on the UI thread with no parent window. On Linux the
xdg-desktop-portal call blocked the event loop until the dialog closed,
and the unparented dialog could open behind WordCraft, so the app looked
frozen.

The desktop app now shows rfd::AsyncFileDialog parented to the window
(set_parent with eframe's Frame, no unsafe) and waits for the answer on
a background thread, which hands it back through a channel and wakes the
UI. ui-egui gains a backend-agnostic Services::file_dialog hook: the app
remembers what the dialog was for and finishes it when the answer
arrives (open the file, save, export, insert or change the picture, turn
AutoSave on). "Save changes?" -> Save on a new document now runs the
held-back New/Open/Close only once the later Save As has saved; a
cancelled Save As cancels it. Only one file dialog is open at a time;
further clicks are ignored until it is answered.

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

* Chinese interface: embed the Chinese UI font in releases, fall back to an installed one (#241)

The release workflow pinned storytold/craft-fonts at abb8331, the commit that added the
Japanese fonts. Noto Sans CJK SC came one commit later (8dcdacd), so release builds embedded
only the Japanese faces and the Simplified Chinese interface fell back to BIZ UDPGothic, which
lacks about 157 simplified-only hanzi (页, 选, 删 …) and drew them as boxes. Document text was
unaffected because it uses the system font fallback.

- Bump the craft-fonts pin in the release and FreeBSD workflows to 8dcdacd.
- With CRAFT_FONTS_REQUIRED, a native build now fails when the craft-fonts manifest has no
  Jpan or Hans font, so an outdated pin can't ship silently again.
- When no embedded face covers the interface language, the UI font families get an installed
  CJK font (desktop only): a short per-platform list of files in the system font folders
  (Windows Deng/msyh/simhei/simsun, macOS PingFang/STHeiti, Linux Noto Sans CJK/WenQuanYi),
  header-checked, size-capped, and the face chosen by coverage (a SC face in a pan-CJK
  collection), so egui never sees a file it can't parse.

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

* Add a System interface theme that follows the OS appearance (#115)

The interface theme is now a three-way choice: Use system setting,
Light or Dark, in File > Options > General and in a new Interface Theme
menu in View > Dark Mode. System follows the light/dark appearance egui
reports (winit on macOS, Windows and Linux; prefers-color-scheme on the
web) at launch and live, and falls back to light when the OS reports
none. egui's own theme is now pinned to ours, so an OS change no longer
swaps in egui's unstyled style.

Saved preferences migrate: the old "dark" switch becomes Dark or Light
and is never written again. Light stays the default for new installs.
New ui.theme command (light/dark/system); ui.dark still sets Light/Dark.
The dark page (View > Switch Modes) is unchanged.

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

* #248: read the installed CJK UI font only when CJK text is shown

A build without craft-fonts read a large system CJK font on the first frame even
for an English interface. Now it is loaded only for a CJK interface language, or
once File > Options shows the language list (names in their own scripts).

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-10 13:05:44 -04:00
Dmitrii Khlapov 302528d67e
Web: use the browser clock everywhere on wasm (insert weekday, PDF dates, layout timing, UI timers) — follow-up to #214 (#230)
#214 gave now_iso the browser's clock on wasm; the other clock helpers were
still stubs there: Insert Date's weekday was always Thursday, the PDF
creation date was left out, and the layout and UI millisecond clocks
returned 0, so browser AutoSave's timer never fired, the caret didn't blink
and status messages never cleared.

The engine's clock is now one now_unix() (std on native, js_sys::Date in the
browser, as #214 does) that now_iso and the weekday share; the pdf, layout
and ui-egui clocks read js_sys::Date the same way on wasm. Refs #188.

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:20:56 -04:00
Fabio Lussi 58ae1d5685
Use the browser clock for now_iso on wasm (comment and tracked-change dates) (#214)
Fixes #188.

Co-authored-by: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 09:27:02 -04:00
LloydNicholson 24388e2c24
macOS: open documents from Finder, the Dock and `open -a` (#27) (#223)
* macOS: open documents from Finder, the Dock and `open -a` (#27)

Double-clicking a document, Open With, dropping on the Dock icon and
`open -a WordCraft letter.docx` send the app a kAEOpenDocuments Apple
event instead of command-line paths. winit 0.30 doesn't handle it, so
AppKit answered "WordCraft cannot open files in the Word Document format".

Register the handler with the audited fmv-macos-events crate that PdfCraft
and PhotoCraft already use: it does the Objective-C interop (unsafe, which
this workspace forbids) behind a safe main-thread API and leaves winit's
delegate alone. Opened paths go through `file.open` like File > Open, so
unsaved changes are asked about first; a quit event closes the window,
which asks the same. The registration lives in main() until the event loop
returns, so the event that launched the app is caught too.

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

* macOS open: say in the status bar when a file can't be opened, and leave Backstage after opening

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

---------

Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 09:26:52 -04:00
Saeed Azarehgar 5d86dec250
Right-to-left and Persian text support (bidi, shaping, RTL paragraphs, DOCX) (#207)
* feat(doc,docx): complex-script run properties and RTL OOXML round-trip

Model the formatting Word applies to Persian, Arabic and Hebrew text:
cs, font_cs, size_cs, bold_cs, italic_cs and lang_bidi on CharProps,
resolved with Word's fallbacks (ResolvedChar::complex). Size, bold and
italic pair with their complex-script values so a run's size or bold
also covers its Persian characters. Add doc::bidi script helpers, and
keep ZWNJ and Arabic marks inside words for word selection.

DOCX: read and write w:rFonts/@w:cs, w:szCs, w:bCs, w:iCs, w:cs and
w:lang/@w:bidi in schema order; stop using the cs font as the Latin font.
Tests round-trip Persian text with ZWNJ, digits and mixed Latin.

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

* feat(fonts): directional shaping and Persian font fallback

Add shape_run to shape a run in a known bidi direction (Arabic joining,
mirrored brackets). Prefer Persian-capable fonts (Vazirmatn, Noto Naskh/
Sans Arabic…) for right-to-left fallback, pick bold/italic fallback faces,
and map common Persian document fonts (B Nazanin, B Lotus, B Titr,
IRANSans…) to open substitutes.

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

* feat(layout): Unicode Bidirectional Algorithm and right-to-left paragraphs

Run UAX #9 per paragraph (unicode-bidi), shape each level run in its
direction, keep marks and ZWNJ with their letter and a word in one
fallback face. After line breaking, apply rules L1/L2 per line and give
each cluster a visual box; right-to-left paragraphs are laid out from
their start edge and mirrored, so indents, tabs, alignment, list labels,
drop caps, floats and borders sit on the right.

Caret, hit testing and selection use visual geometry (split selections,
caret at bidi boundaries, DocLayout::visual_step for arrow keys). Display
items carry glyph-to-text ranges, which the PDF writer now uses. Plain
left-to-right lines are unchanged. No hyphenation of RTL words.

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

* feat(engine,ui): paragraph direction commands and RTL editing

Add para.ltr / para.rtl (Word's text direction buttons) that change only
the reading order, and make the alignment buttons visual in right-to-left
paragraphs (para.align also takes start/end). Arrow keys move visually in
bidirectional text, Ctrl+arrows follow reading order, and Bold/Italic/
Size/Font cover complex-script text. Ribbon buttons with code-drawn
icons, a mirrored ruler for RTL paragraphs, translations, catalog,
parity and roadmap updates.

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

* chore(contributors): add Saeed Azarehgar to people.toml

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

* fix(engine): para.align with an explicit value sets it, without toggling

Only the alignment buttons toggle back to the start edge when pressed
again. para.align (used by the Paragraph dialog, scripts and agents) now
sets the requested alignment, as seen on the page, in each selected
paragraph's own direction. Previously pressing OK in the dialog with
Center or Right selected reset the paragraph to the start edge.

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

* feat(ui): Direction option in the Paragraph dialog

Add Word's Direction (Right-to-left / Left-to-right) to the Paragraph
dialog's General section. The dialog now shows alignment and Left/Right
indents as seen on the page, mapping them to the paragraph's logical
start/end values on OK, so an unchanged OK no longer flips a
right-to-left paragraph. OK logic moved to apply_paragraph, with tests;
ja/zh translations.

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

* test(layout): allow hanging trailing spaces in the RTL display test

A wrapped right-to-left line's trailing spaces hang past its end edge
(the left margin), as left-to-right lines hang them past the right. The
test failed whenever the loaded fallback fonts made a line end there;
it now checks only visible glyphs.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 09:26:41 -04:00
Eduardo Soares 663904a2e2
Import Word 97-2003 .doc files (#28) (#145)
* D0+D1: open Word 97-2003 .doc — container, FIB, piece table text (#28)

New wordcraft-docbin crate (L2) reads the OLE2 container via the cfb
crate (MIT) and parses the FIB and piece table from the [MS-DOC]
specification: text pieces decode as UTF-16LE or Windows-1252 (table
moved to wordcraft-doc::encoding), subdocument CP ranges, and Word's
special characters map to the model's (tabs, breaks, hyphens). .doc/.dot
registered in OPEN_EXTS and the I/O dispatch. Encrypted and Word 6/95
files give clear errors; every length is checked and proptest
truncation/flips never panic.

* D2: .doc formatting — stylesheet, fonts, CHPX/PAPX FKPs, sprms (#28)

Sprm decoder with operand-size rules (incl. sprmTDefTable/sprmPChgTabs
exceptions and the Prm0 isprm table), bin tables + ChpxFkp/PapxFkp page
lookup, STSH styles (StdfBase layout validated against real Word files:
bchUpe == cbStd, sti 0 = Normal) and the SttbfFfn font table in both
extended and self-delimiting flavours. The CP walk now splits runs on
formatting changes and resolves paragraph styles: bold/italic/strike/
underline/caps/hidden, colours (Ico palette + COLORREF), half-point
sizes, super/subscript, fonts, highlights; alignment, indents, spacing,
keep options, outline levels. Unknown sprms are skipped leniently;
an env-gated corpus test opens real files (styles + text asserted).

* D3: .doc structure — sections, headers/footers, tables, lists (#28)

- Sections: PlcfSed/Sepx with 16-bit measurement sprms (sprmSXaPage & co),
  explicit sprmSBOrientation with a width/height fallback; even/odd/first
  header and footer stories from Plcfhdd become document parts.
- Tables: sprmTDefTable grids (operand length fixed to cb+1 per the spec),
  horizontal merges (sprmTMerge), vertical merges (sprmTVertMerge), cell
  shading (sprmTSetShd / sprmTDefTableShd80), cell widths, valign, row
  height/header/cant-split, and table borders (sprmTTableBorders80 Brc80s).
- Lists: PlfLst (LSTF + appended LVLs) and PlfLfo (LFO + LFOLVL overrides)
  resolved into the numbering model; sprmPIlvl/sprmPIlfo mark paragraphs.
- Combined fixture (intro + table + list + 2 sections + header) round-trips
  through Document JSON; hostile proptests now mutate the structured fixture.

* D4: .doc notes, fields, bookmarks and inline pictures (#28)

- Footnotes/endnotes: PlcffndRef/PlcfEndRef reference CPs paired with the
  PlcffndTxt/PlcfEndTxt story ranges; each story walks into a Footnote or
  Endnote part and the 0x02 anchor becomes a NoteRef object.
- Fields: 0x13/0x14/0x15 state machine in the walker — instruction text is
  hidden, the cached result kept; HYPERLINK fields keep their result as
  linked runs, everything else becomes one Field object.
- Bookmarks: SttbfBkmk names + Plcfbkf/Plcfbkl CPs become BookmarkStart/
  BookmarkEnd objects at their anchors.
- Pictures: sprmCPicLocation points into the Data stream; the PICF header
  gives the display size (dxaGoal/dyaGoal scaled by mx/my) and the payload
  is format-sniffed (PNG/JPEG/GIF/BMP; metafiles are dropped with a warn).
- Hostile proptests now truncate/flip a fixture with all of the above.

* D5: .doc import hardening, caps and roadmap (#28)

- Cap inline pictures at 5 000 per document.
- Engine test: .doc/.dot bytes dispatch through the Word 97-2003 reader
  (garbage must error, never panic or open empty).
- ROADMAP: Word 97-2003 import row (60% — text, formatting, styles,
  sections, headers/footers, tables, lists, notes, fields, bookmarks,
  raster pictures).

Real-corpus validation stays opt-in: WORDCRAFT_DOCBIN_FIXTURES=<dir>
cargo test -p wordcraft-docbin corpus_files_open.

* Corpus fixes: compressed-piece FC space, PAPX query point, none-level labels (#28)

Validated against two real files (kept locally under plan/word/fixtures,
gitignored): a 1 MB Lorem-ipsum sample and a 2003 styles/lists/table
sample.

- FKP bin tables and pages address the WordDocument stream in true byte
  offsets; a compressed piece stores its base fc doubled (byte = fc/2),
  so fc_of_cp now unwinds the doubling. Before, every PAPX/CHPX lookup in
  compressed (cp1252) files landed on the wrong entry — tables collapsed
  to one row, lists and heading styles were lost.
- The paragraph-properties query now uses the paragraph mark's own FC
  (inside the paragraph) instead of the FC after it, which picked up the
  next paragraph's FKP entry.
- Fixture papx entries re-keyed to paragraph-start CPs, the real rgfc
  convention.
- Counters::next_label returns nothing for NumFormat::None levels (Word's
  numFmt none shows no label; also fixes a latent docx-reader case).
- Symbol/Wingdings bullet chars map to Unicode glyphs (• ▪ ○ ●).

With this, both corpus files render headings, styles, nested and numbered
lists, the tables and the PAGE field correctly; metafile-wrapped images
and OLE objects stay dropped by policy (logged).

* docbin: bound the text walk by real text; dedupe and cap pictures (#28)

Hostile CP numbers no longer drive the work:
- header/footnote/endnote story ranges from the Table stream are clamped to their
  subdocument (ccpHdd/ccpFtn/ccpEdn) with checked base additions, and every walk stops at
  the last piece's CP; a story spanning [0, 4e9) used to push billions of U+FFFD;
- characters are found by binary search over the pieces (PieceTable::char_at), and an
  unreadable stretch (gap, truncated or out-of-stream piece) is skipped in one step rather
  than one U+FFFD per CP; the old per-character scan was O(chars x pieces);
- the piece CPs are made non-decreasing at parse, so inverted pieces are empty and binary
  search stays sound; surrogate pairs now decode;
- note references are looked up by binary search; the style's CharProps is borrowed, not
  cloned, per character;
- pictures are interned by fc_pic (5000 anchors on one picture share one media entry and
  one copy) and the bytes copied per document are capped at 256 MB.

Each fix has a crafted-bytes regression test; against the previous code the story,
piece-claim, last-piece and many-pieces tests hang (killed at 60 s) and the shared
picture test copies the picture once per anchor (80 GB).

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

* docbin: checked offset arithmetic in lists, sections and table rows (#28)

- list.rs: PlfLst/PlfLfo/LVL offsets (at + 2 + cLst*28, at + 4 + lfoMac*16, off += len,
  ...) saturate, which matters on 32-bit targets (wasm32) where fc + n can overflow usize;
  the LFO override path no longer indexes abstracts directly.
- sections.rs: the Sepx range (fc_sepx + 2 + cb) is checked; Plcfhdd is read only as far
  as the stories a capped section count can use (it used to reserve lcb/4 entries).
- table.rs: sprmTDyaRowHeight = i16::MIN negated in i16 (overflow panic in debug builds);
  now -(v as f32).

Regression tests: the row height test failed before (overflow panic with debug assertions, a 0 pt height without); the list test pins the saturating
offsets (it only overflowed on 32-bit targets).

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

* Format the merged OPEN_EXTS list

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-10 08:36:38 -04:00
Dr. Gled Guri 43c0be5735
Zotero integration: cite while you write, with a Zotero tab (#189)
* 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>
2026-10-10 08:35:59 -04:00
Lucio bded38dd23
Control port: require a per-window key (split from #54) (#124)
* MCP bridge: send the control key, to loopback addresses only

`wordcraft-cli mcp --connect` now sends the app's control key with every
request. It takes the key from WORDCRAFT_CONTROL_KEY, or else from the
key file the app writes for that port, and reads it again on every new
connection, also after an `unauthorized` reply (the app restarted with
a new key).

The key goes only to loopback addresses: 127.0.0.0/8, ::1 and
`localhost`, and only when the connection really leads to a loopback
address. For any other host the bridge sends no key, prints why at
start, and turns the app's `unauthorized` into an error that says the
key was not sent and how to reach the app (through 127.0.0.1, for
example an SSH tunnel). Other host names are never resolved for this.

The new `wordcraft_mcp::control_key` module holds the rule the app and
the bridge share: the settings folder (the one with ui.json), the
Flatpak instance id and `<settings>/control-key.<instance>`
(`local-<port>` outside Flatpak). No new dependency; wasm-safe.

* Control port: every request needs the window's key

Any local program, and possibly a web page, could drive the app through
its loopback control port. Each window started with --control now has
a key:

- once the port is bound, the app writes a random key (32 bytes from
  the OS, as hex) to <settings>/control-key.<instance>, mode 0600 on
  Unix, replacing a stale file; it removes the file on a clean exit. A
  failed bind writes nothing. <instance> is the Flatpak instance id, or
  local-<port> outside Flatpak (wordcraft_mcp::control_key, the rule
  the MCP bridge reads it with). The settings folder is the one that
  holds ui.json; prefs_path now uses the same helper.
- every request needs the key: top-level "key", or
  {"method":"auth","key":...} once per connection. Only auth reads
  params.key; elsewhere it stays an ordinary parameter (ui.key). The key
  is checked on every request, in time that does not depend on where a
  wrong key differs. A missing or wrong key gets
  {"ok":false,"error":"unauthorized"} and the connection is closed.
- a line over 1 MiB gets "line too long" and the connection is closed
  (the rest of the line is drained first, so the client reads the
  error).
- a connection that sends no authorised request within 30 s of being
  accepted is closed; blank lines and bad JSON do not extend that
  limit. After that it may stay idle.

* Docs: control-port keys

docs/control-protocol.md gets a Keys section: where the key file is
(per platform and per instance), top-level "key" or auth once per
connection, `unauthorized` and the closed connection, the 1 MiB line
limit, the 30 s limit for the first authorised request, and how the MCP
bridge finds the key and sends it only to loopback addresses (an SSH
tunnel for an app on another machine). docs/mcp.md points there, and
the agent guides (AGENTS.md, CLAUDE.md) show the key in their control
channel example.

* Control key: the key-file helpers in their own small crate

wordcraft-control-key (standard library only, Standalone in the layering
table) holds where the key file lives and how to read it. The app and the
MCP bridge both use it, so the app no longer depends on wordcraft-mcp.

* Cargo.lock: wordcraft-control-key follows the workspace version (0.4.0)

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

---------

Co-authored-by: lpgn <2556951+lpgn@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:56:41 -04:00
AiBizzApps 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
AiBizzApps c0ccda288c
Find/Replace skips text deleted under Track Changes (#89); a failed command keeps undo history (#153)
* Find/Replace: skip text deleted under Track Changes

With Track Changes on, Replace marks the match deleted and leaves it in
the paragraph text, then inserts the replacement in front of it. The
search helper matched the raw paragraph text, so the next step found that
same deleted "cat" right after the caret and replaced it again: four
Replaces on "one cat two cat" gave "one dogdogdogdog two cat" and
`remaining` never dropped.

`search` (used by Find, Find Next/Previous, Replace, its `remaining`
count and Replace All) now skips any match that overlaps a run marked
deleted, so deleted text is never found, as in Word. A match that would
run across deleted text is skipped too: that string is not in the
document as it now reads.

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

* Undo: a failed command keeps the redo stack and the oldest undo step

`run` checkpoints a mutating command before running it, and the
checkpoint clears the redo stack (and, once the history is at its limit,
drops the oldest step). When the command then failed, the error path put
back the document, selection and typing group and truncated the history,
but the cleared redo stack stayed cleared. Insert, Undo, then any
command that fails on its parameters (`text.insert {}`,
`para.align {value:"bogus"}`) made the undone work unrecoverable. At the
limit, truncating to the old length also kept the failed command's empty
step and lost the oldest real one.

The checkpoint now hands back what it removed, and the error path
restores both. join_next is deliberately left consumed: the drag that
set it re-arms it every frame, and restoring it would merge the next
unrelated command into the drag's undo step.

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

* Find/Replace: find live text next to a tracked deletion

The previous fix rejected any regex match that overlapped tracked-deleted
text, but `find_iter` had already consumed those positions, so a valid
match overlapping the rejected one was never tried. "aaa" with the first
"a" deleted reads "aa", yet Find "aa" found nothing (raw 0..2 was
rejected and the scan resumed at 2) and Replace All replaced nothing.
Searching the raw text also let deleted characters decide word
boundaries: deleting the space in "cat fish" still matched whole-word
"fish".

`search` now builds each paragraph as it reads, without deleted runs,
and searches each stretch of live text separately with
`regex_automata::Input::range`. A match stays within one stretch, as
before, and `\b`, `^` and `$` see the neighbouring live text, so a
deletion neither creates nor hides a word boundary. `regex::Regex` cannot
bound a search's end without slicing the haystack, which turns the slice
end into a word boundary; regex-automata is the engine `regex` already
uses (0.4.18, already in the lockfile), so no new code is built.

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

* Undo: a failed command restores history changed by nested commands

The previous fix had the checkpoint report what it evicted and put that
back on failure, but it only saw the outer command's own checkpoint.
Commands run nested commands, and a nested one that succeeds checkpoints
too. `file.inspect {"remove":["comments","revisions"]}` in a
comments-only document runs `review.deleteComment` (allowed) and then
`review.acceptAll` (refused). With 499 undo steps, the nested checkpoint
evicted the oldest step and the rollback, truncating to 499, kept the
outer command's empty step instead. At 500, only one of the two
evictions came back. Nested undo, redo and version restore change the
stacks in ways a length cannot describe either.

Undo steps are now `Arc<Undo>`, and `run` snapshots both stacks whole
before a mutating command and puts them back on failure, so the
history is exact whatever ran inside. The snapshot copies one pointer
per step, under 1,000 refcount bumps and an 8 KB allocation at
MAX_UNDO, beside the two document clones every mutating command
already makes. Undo and Redo are not mutating commands, so a top-level
Undo still moves its document out of the step without cloning it. Only
an undo nested inside a mutating command (a macro) clones, because the
snapshot still holds the step. This replaces the eviction-return
plumbing, so `checkpoint` is back to its original form.

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

* Tests: compare undo timelines without comment timestamps

failed_command_restores_history_changed_by_nested_commands builds the
same session twice and compares their documents. Comments are stamped
with the time to the second, so when the clock crossed a second between
the two builds the documents differed and the test failed (about 1 run
in 12). Clear the stamp in the compared copies; the test still checks
every other part of each document in the timeline.

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

* Undo: wrap the typing-parity undo step in Arc after merging main

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-10 06:34:35 -04:00
Brandon Thomas a1caf22a0b Bump version to 0.4.0 (from 0.3.0) for release; refresh contributors
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:02:32 -04:00
DWAA1660 d7d9702bda
Interface languages: follow the system, or pick one in Options (#8) (#12)
WordCraft's interface can now be shown in Simplified Chinese, Traditional
Chinese or Japanese as well as English. The new `language` setting is
`auto` by default: the UI follows the operating system's preferred
languages (Windows display language, macOS preferred languages,
LANG/LC_* on Unix, via sys-locale; WORDCRAFT_LANGUAGE overrides) when
there is a catalog for one of them, and English otherwise. File > Options
> Interface language picks one by hand, and the `ui.language` command does
the same for scripts and agents. The setting persists with the other UI
preferences.

The i18n module is PdfCraft's (catalog format, parser, locale matching),
trimmed to plain entries. English strings in the code are the keys; the
ribbon helpers (group, big, small, split, menu_button, menu items,
tooltips and ribbon locations), tabs, Backstage, Options, dialogs, panes,
the status bar, the context menu and command search translate at draw
time. Command ids, documents, the CLI, MCP and the control channel stay
English. Catalogs hold ~780 entries per language, written clean-room from
the English meaning.

Chinese interfaces put the craft-fonts Chinese face before the Japanese
one so a line never mixes glyph styles; fonts are reinstalled when the
language changes.

Fixes #8.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <shared2@storyteller.ai>
2026-10-09 09:22:42 -04:00
Brandon Thomas 7584b9b293 Bump version to 0.3.0 (from 0.2.0) for release
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:02:16 -04:00
Brandon Thomas 2ed3895276 Bump version to 0.2.0 (from 0.1.0) for release
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:13:16 -04:00
Brandon Thomas 906d4faca3
About: contributor and model credits compiled in (grab bag + table, self-submitted names) (#6)
* About: contributors compiled in from contributors.json (grab bag + table, sortable, name toggle)

Adds About ▸ Contributors and About ▸ Models tabs per the craftrules standard
(standards/contributors.md): contributors/contributors.json is generated by
craftrules/scripts/contributors.py and baked into the binary by crates/ui-egui/build.rs as
static tables. Each contributor line: @username, opt-in display/real name, PRs, commits,
lines +/−/Δ, binary assets +/−, first/last commit. Sortable by every column; alphabetical
ignores case and the leading @. `ui.dialog` also accepts `contributors` / `models` to open
the About window on that tab.

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

* Contributors: self-submitted names in contributors/people.toml; AGENTS.md credit-yourself section

build.rs reads contributors/people.toml next to the generated contributors.json and shows a name only
for entries the generator verified as written by that person (craftrules standards/contributors.md).

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

* Contributors: regenerate (verified people.toml entries)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:17:02 -04:00
Brandon Thomas 7b4a526fdf Release pipeline (macOS universal signed/notarized DMG, Windows x64/x86 MSI, Linux AppImage/deb/rpm/tar/Flatpak, FreeBSD, web zip) adapted from DesignCraft; Windows exe resources; version in --version/About; multithreaded page raster
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:50:50 -04:00
Brandon Thomas 72262a2ea9 M3/M6/M12: PDF export (krilla), TXT/MD/HTML/RTF/ODT import+export, citations & bibliography, index, table of figures, cross-references, tools (repeat, macros, AutoCorrect, compare, accessibility, inspector, protection, versions, Quick Parts)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:57:36 -04:00
Brandon Thomas c29182e8bc M1: DOCX read/write (OOXML) wired into open/save; floating objects; picture & shape tools; mail merge, envelopes, labels; contextual spacing; export without proofing marks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:46:40 -04:00
Brandon Thomas 435194f7c2 M0.9–M0.12: CLI, MCP server, xtask (layers/assets/parity/ci), icon placeholder, AGENTS/ATTRIBUTION/NOTICE, protocol docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:21:35 -04:00
Brandon Thomas 98bbfbed4e M0.8: Word-style egui UI — ribbon (all tabs), page canvas, rulers, panes, dialogs, Backstage, control channel; desktop app
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 23:16:02 -04:00
Brandon Thomas b8dd46ccb5 M0.7: engine — session, undo, 200+ commands, Word feature catalog, samples
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:47:26 -04:00
Brandon Thomas d1e80df4da M0.1–M0.6: workspace, geom, document model, fonts, layout, render
Clean-room foundations for WordCraft: Arc-shared document model with
paragraph/run editing, styles, lists, sections and tables; Word-style
font substitution; first-fit line breaking with tabs, lists, alignment,
pagination, tables, headers/footers and hit testing; vello_cpu page
rasteriser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 22:30:16 -04:00