Commit Graph
4 Commits
Author SHA1 Message Date
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
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
Brandon ThomasandClaude Opus 5.5 d95e09ba25 Clippy/fmt clean workspace, ROADMAP with honest parity estimate; xtask ci green
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 00:11:08 -04:00
Brandon ThomasandClaude Opus 5.5 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