Commit Graph

15 Commits

Author SHA1 Message Date
Brandon Thomas eb31860917
Progress docs: target-app parity, gaps, alpha gate, readiness by audience (#277)
Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, and feature, format, layout, hardware, UI and localization parity docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:11:53 -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
Dmytro 0811a80f31
Add Ukrainian (uk) interface translation (#91)
* i18n #85: add Ukrainian interface translation

Translate the existing 771-entry interface catalog and register Ukrainian
for locale detection and persisted selection. Check full catalog coverage,
count-neutral labels, preference persistence, and bundled Ukrainian glyph
coverage. Validate the translation against Ukrainian dictionaries.

Co-authored-by: Codex <noreply@openai.com>

* i18n: Ukrainian rows for strings added on main

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

* i18n: Ukrainian label for Character Border (lands after #60)

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

---------

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 07:18:14 -04:00
Geronimo Castaño 7d592ce19a
Add Spanish interface language, and make ribbon overflow visible (#129)
* Spanish interface language (es)

Adds a Spanish catalog beside the Chinese and Japanese ones and registers
it in LANGUAGES, so it appears in File ▸ Options ▸ Interface language and
follows Spanish system locales (es-ES, es-MX, es-419 …).

Neutral Spanish: tú for sentences, infinitives for commands, sentence case.
Written from the English meaning; no Microsoft Word localisation was
consulted or copied. Covers the 771 keys of the existing catalogs, plus the
two ribbon-edge tooltips added in the next commit.

The bundled-catalog and every-command-translated tests pass.

* Ribbon: show and scroll past the edge when a tab overflows

The ribbon scrolls horizontally, but its scrollbar is hidden, so groups that
run past the window edge were clipped with no sign they existed. Spanish
labels make the Review and Home tabs wider than the window at 1440 pt.

Overflowing rows now show ‹ and › chevrons at the edges. Clicking one scrolls
the row by 160 pt; the chevron disappears at each end. The tooltips are in
the Spanish catalog and show in English in other languages until translated.

* i18n: Spanish rows for strings added on main; ribbon scroll labels 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-10 07:07:30 -04:00
DWAA1660 02e2a51bb4
LaTeX (.tex) import and export (#7) (#11)
* LaTeX (.tex) import and export (#7)

Open and save LaTeX source as a document format, next to Markdown and
HTML. Export writes a standalone article (fontenc, ulem, xcolor,
multirow, amsmath, hyperref, loaded only when used): unnumbered sections,
itemize/enumerate, quote, center/flushright, verbatim, tabular with the
document's column widths and merged cells, equations as $..$ / \[..\],
character formatting, colours and links; metadata goes to \hypersetup.
Import reads the everyday subset: preamble \title/\author, sectioning
(\part/\chapter shift levels), paragraphs, lists with labels, quote,
alignment environments, verbatim-like environments, tabular with
\multicolumn/\multirow/\cellcolor and p{} widths, inline and display
math, formatting commands and declarations, colours, links, refs,
citations, footnotes, accents, TeX ligatures and special characters.
Unknown commands keep their arguments' text; layout and definition
commands are dropped with their arguments. Nesting is depth-limited and
input is capped, so hostile files can't overflow the stack.

The flow model gains an Equation inline so equations survive LaTeX round
trips; the other text formats write their linear text as before.

Wired into open/save dispatch, the file dialog filter, Backstage Export,
the web app, CLI/MCP help, docs and the Linux desktop entry.

Fixes #7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FczntrDNYd6DupyBdpbL5

* #7: bound LaTeX column specs and equation export (review)

- spec_widths: `*{n}{…}` repeats recurse with a depth argument and stop at
  MAX_NEST, so a deeply nested column spec no longer overflows the stack or
  exhausts memory. Column counts saturate and widths stop at MAX_COLS.
- Column specs read from the source are capped at MAX_SPEC (4096) chars.
- balanced() looks groups up in a table of closers built in one stack pass,
  so linear_to_latex no longer rescans to the end of the string for every
  unmatched `(` (quadratic: 50k took seconds). spec_widths uses it too.
- env_name restores the position when `\begin{` / `\end{` has no `}`
  within 64 chars, so the `{` is no longer lost.
- Regression tests: the 100k-deep `*{1}{` spec and a 200k-`(` equation in
  deep_nesting_is_bounded; unclosed_begin_keeps_its_brace and
  spec_widths_repeats_are_bounded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyVbT6Vv4BwWZ4hY8973qD

* #7: linear-time labels, brackets and \begin{; no commands from the document in exports

Applies @HaringIbon's review suggestions:

1. Parser::anchor keeps at most MAX_ANCHOR_RUN (64) bookmarks in a row with
   no text between them; text() looked back past every one, so a file of
   labels imported in quadratic time. Labels separated by text are all kept.
2. linear_to_latex_at checks for the `/` of a fraction before copying the
   group out, so deeply nested matched brackets export in linear time.
3. env_name searches only the next 65 bytes for `}`, so a run of unclosed
   `\begin{` imports in linear time.
4. An equation's `\name` is written as a command only when it is a known
   symbol, function or font/spacing command (MATH_COMMANDS); anything else
   becomes `\backslash name`. Two carets are never written side by side in
   equations or anchor names, since TeX reads `^^5c` as a backslash. Compiling
   an export can no longer `\input` files or `\write18` named by the document.

Regression tests: three cases added to deep_nesting_is_bounded, plus
latex_export_carries_no_commands_from_the_document and
latex_labels_in_a_row_are_read_in_linear_time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyVbT6Vv4BwWZ4hY8973qD

* Merge follow-ups: FTable.borderless (from #102) in the LaTeX importer; rustfmt

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:57:01 -04:00
Eduardo Soares ae79726ab6
i18n: add Brazilian Portuguese (pt-br) interface translation (#127) (#128)
771-entry pt-br.tsv catalog written clean-room from the English labels,
one LANGUAGES row, locale/persistence/coverage/font regression tests,
and README, ROADMAP, control-protocol and ATTRIBUTION updates.
2026-10-10 06:17:23 -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
Gert Pellin - GPE 9b6f42dd5f
Desktop: write log records to standard error and a rotating log file (#17)
* Desktop: write log records to standard error and a rotating log file

The desktop app installed no logger, so every log::warn!/log::error! in
the workspace was discarded: an unreadable part of a .docx, a picture or
font a PDF export had to leave out, a PNG that would not encode. A launch
from a desktop menu has no terminal either, and a panic's message went to
that missing terminal, so there was nothing to attach to a bug report.

apps/wordcraft/src/logging.rs (no new dependency):
- standard error plus logs/wordcraft.log next to the preferences
  (~/.config/wordcraft/logs, or under $XDG_CONFIG_HOME); each launch
  rotates the previous log to wordcraft.1.log and .2.log, so the log of
  a run that crashed survives the next start; the file is capped at 16 MiB
- records logged before the settings directory is known are buffered
  (bounded) and written once the file is attached
- default levels: info for wordcraft*, warn for everything else;
  RUST_LOG replaces them with env_logger-style directives
- never panics: an unusable directory or a failed write leaves standard
  error as the only sink
- runs with WORDCRAFT_NO_PREFS (agents' test runs) log to standard error
  only, so they don't rotate away the user's own logs
- a panic hook reports every panic, also those Session::run's guard
  recovers from, through the logger; text the message quotes between
  backticks (std quotes the string it failed to slice, which can be the
  user's writing) is left out

The logger is installed first in main, before the panic hook, and the
file is opened after argument parsing (--version leaves no file). The
control server's and file-open messages go through the logger and drop
their "wordcraft: " prefix, which the record's target now shows.
Documented in README.md (Logs).

* Desktop: no eprintln! on the log and panic paths

eprintln! panics when standard error is a broken pipe (a GUI launch on
Windows), and a panic inside the panic hook aborts. The "no log file"
message goes through log::warn! (standard error only by then); the panic
hook's fallback writes to standard error with write_fmt and ignores the
error. Same fix as storytold/vectorcraft#465.

---------

Co-authored-by: Brandon Thomas <shared2@storyteller.ai>
2026-10-09 09:22:04 -04:00
DWAA1660 f2a8420679
Default Windows startup to DirectX 12 (#50)
eframe's default wgpu instance enables PRIMARY | GL on Windows. Creating
the OpenGL backend can crash inside AMD's atio6axx.dll before the window
appears, so the app exits silently at launch. When WGPU_BACKEND is not
set, the Windows instance now enables only DirectX 12; an explicit
WGPU_BACKEND is still honoured. macOS, Linux and web are unchanged.

Same fix as storytold/designcraft#168 and storytold/filmcraft#179.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 08:49:04 -04:00
Brandon Thomas e04372aa83
README Downloads: where to get it first, then builds with Windows before macOS (#26)
Also fix the .deb/.rpm description in packaging/linux/nfpm.yaml, which still described PhotoCraft.

Co-authored-by: Brandon Thomas <shared2@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:51:15 -04:00
Brandon Thomas b830fae017
Release: Flatpak x86_64 + aarch64 bundles, AppImage zsync auto-update, README Downloads (#22)
- packaging/linux/package.sh: embed gh-releases-zsync update information in each AppImage
  (appimagetool -u) and write the .zsync beside it; the Linux job installs zsync and checks both.
- New flatpak job (x86_64 on ubuntu-24.04, aarch64 on ubuntu-24.04-arm) repackages the Linux
  tarball with packaging/linux/flatpak-bundle.sh and ai.storyteller.wordcraft.bundle.yml; no Rust
  build, no release environment. The release job needs it.
- version, Linux and web jobs drop `environment: release` (they sign nothing), so a branch
  workflow_dispatch dry-runs every unsigned job; the release job still needs the signed jobs.
- FreeBSD release build embeds craft-fonts like the other desktop builds and ships its licences.
- Flathub manifest: same finish-args as the bundle (adds xdg-documents), builds the CLI too;
  packaging-lint checks both manifests agree.
- README: Downloads section; docs/releasing.md updated.

Co-authored-by: Brandon Thomas <shared2@storyteller.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:38:25 -04:00
Brandon Thomas 5dce7f5ed1
Rename PrintCraft to PdfCraft (storytold/pdfcraft) (#3)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:46:02 -04:00
Brandon Thomas 6e9dc4bab4 M7/M14: comment balloons in the margin; ROADMAP alpha and parity estimates; README Review and Layout screenshots; cargo xtask alias
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 01:38:55 -04:00
Brandon Thomas f3dbc31a57 README with screenshots, Crafting Apps table, community links; proofing ignores deletions/objects; comment shading; hhea line metrics; watermark tuning
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:18:42 -04:00
Brandon Thomas 4e1fb3aba9 first commit 2026-10-05 20:23:39 -04:00