Word brings a document's alternative format chunks (w:altChunk) in where they
stand when it opens the file; WordCraft left them out, so a document made of
one (as generators and mail-merge tools write them) opened empty.
- docx: read_with/ReadOptions with a ChunkImporter for formats other than
WordprocessingML; nested Word documents are read here (at most 4 deep,
256 chunks and 512 MB of chunk bytes a file, XML depth counted on).
- doc: Document::adopt (moved from paste) brings another document's media,
lists, styles, notes, comments and revisions along under new ids; pictures
in groups and charts are relinked. Insert > Text from File uses it and now
keeps the file's pictures and lists.
- engine: chunks in MHT, HTML (in their charset), RTF and plain text go
through the format importers.
- formats: MHT web archives open (MIME, quoted-printable/base64, charsets,
pictures by Content-Location or cid). HTML reads style element rules
(simple selectors, cascade, budgets), paragraph margins, indents, line
height, cell widths and borders; an opened page takes Word's web-page
defaults (Times New Roman 12 pt, single lines, black headings, automatic
spacing). html::decode knows the single-byte Windows code pages.
docxide-pdf suite: croatian_regulations_altchunk goes from one empty page to
Word's 8 pages, SSIM 2.7% -> 78.7%; no other document changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
many_formatted_leading_breaks_trim_quickly failed at its 2 s bound on a machine at load ~50
(it takes about a second there). Bounds now sit at least 10x over the fixed code's time on a
loaded machine and far under the old code's on the same input (measured by running the
pre-fix code, capped at 400 s):
- HTML leading breaks: 2 s -> 20 s; ~1 s now, old trim ran past 6 minutes.
- LaTeX nested multicolumn: 2 s -> 10 s; under 1 s now, old unwrapping took 213 s.
- Markdown delimiters: 2 s -> 10 s; under 1 s now, old backwards search past 6 minutes.
- Para::trim blank spans: 1 s -> 10 s; 0.16 s now, old Vec::remove(0) past 6 minutes.
- repeated sheet headers: 2 s -> 10 s; 20 ms now, old numbering past 6 minutes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
Every limit this branch added that leaves data out now says so:
- font names over 256 bytes and links over 8 KiB (HTML, ODT, RTF,
LaTeX, Markdown), and RTF style names cut to 256 bytes;
- runs' links and fonts past the 64 MiB run-string budget;
- spreadsheet rows refused for being over the text budget (ODS and
Grid::push), once per sheet.
model::dropped logs each kind once per imported document; each
importer's entry point starts a new document with begin_import. Tests
capture the warnings (crate::test_log) and check each is logged once,
again for the next document, and not at all when nothing is dropped.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o
Findings 58 and 65. Every span deep-copied its inherited link and font
strings, so one long name or Markdown definition times many spans or
references expanded a small file into gigabytes. Fmt now holds both as
Arc<str> (spans and Markdown references share one copy), names over
256 bytes and links over 8 KiB are dropped at the importers (RTF, HTML,
Markdown, ODT, LaTeX), and the document builder stops giving runs link
and font copies past 64 MiB.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Finding 74: trimming removed each leading whitespace-only inline with Vec::remove(0),
shifting the rest every time. A paragraph opening with many separately formatted line
breaks (HTML <b><br></b><i><br></i>…, LaTeX \\) took quadratic time on open or paste.
The removable prefix is now counted and drained once; the first remaining span is
trimmed as before. 200,000 pairs ran past 60 s before, now milliseconds.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore: resolve SonarCloud maintainability issues
* fix: restore compiling refactors, revert behaviour changes, run cargo fmt
- fontdb::norm: keep the closure for filter (char::is_alphanumeric takes
self, not &self, so it does not fit Iterator::filter).
- story.rs: write_mark_rpr needs &mut self (rev_attrs, rpr_change).
- Revert the viewport meta change and the return 0 added to
workspace_version/sha256, which changed behaviour or masked failures.
- cargo fmt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Import Document in odt page_parts after dropping glob re-export
* fix: apply_kind_properties binds ContentControl; cell_ref keeps char closures
* fix(engine): pass case helpers by value (clippy needless borrows)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
- ODT export reads the tracked insertion/deletion from a Ruby inline's formatting
too (Item::marks and the paragraph's tracked check): an inserted or deleted
phonetic guide keeps its change range around the base text instead of being
written as untracked text.
- HTML export computes the paragraph-start state before the ruby's formatting tags,
so a leading space in the base text becomes instead of a space HTML can
collapse.
- docs/file-format-parity.md: restore main's format rows (the PR had replaced them
with stale copies that lost ODT keep-with-next #2813, TXT #2853 and the HTML
blockquote/MathML details) and add the ruby sentence to the retained .htm/.html
row; restore the rolled-back header change list.
- docs/localization-parity.md: restore the header change list; date the new
revision-history rows 2026-10-21.
- Tests: html_ruby_at_the_paragraph_start_keeps_a_leading_space and
an_inserted_ruby_keeps_its_tracked_insertion.
Validation: cargo fmt --all -- --check; cargo clippy -p wordcraft-formats
--all-targets -- -D warnings; cargo test -p wordcraft-formats -> 204 passed,
0 failed (doctests 0).
Found while testing crates/formats/src/html.rs:29-32 (decode_entity)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
`InlineObject::Ruby` is written as `<ruby>base<rt>reading</rt></ruby>`, in the
base text's run formatting, and `<ruby>`/`<rb>`/`<rt>` are read back into one
ruby object, so docx/doc → HTML → doc keeps the annotation. `<rp>` fallback
parentheses and the tags are not text; unclosed or half-marked ruby (no `<rt>`,
empty `<rt>`, no base) is tolerated and keeps the text it held, with a block
boundary closing the element as a browser does. HTML states no alignment, size or
raise, so import uses the Phonetic Guide's defaults (centred, half the base size,
raised clear of the base text; the run's size, or 10.5 pt when none is stated).
The flow model gains `Inline::Ruby`; formats without ruby (RTF, ODT, Markdown,
LaTeX) write its base text as before. Tests: `html::tests::ruby_*` and
`tests::html_ruby_round_trip`. Docs: file-format-parity (HTML row) and
localization-parity (Japanese row).
Found while testing crates/formats/src/markdown.rs:160-161,463 and crates/formats/src/model.rs:111 (ListInfo)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Found while testing crates/formats/src/html.rs (export, around line 1744; paragraph text writer)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
* fix(formats): support quoted and non-latin charset declarations in HTML decode
* html: clippy (question_mark, collapsible_if) after merging main
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(formats): support relative font size values in HTML font tags
* Drop the duplicate divyanshu2074 entry in people.toml (already on main)
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
LibreOffice puts a paragraph indented on both sides on the clipboard as
<blockquote>, its margins in the page's stylesheet. Paste mapped it to the
centred, italic Quote style. On paste only, it now becomes a Normal
paragraph indented by the blockquote's stylesheet margins (else 0.5 in each
side). Opening .html files still maps blockquote to Quote.
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* perf: lowercase HTML tag names during collection to avoid double allocation
Inline .map(|c| c.to_ascii_lowercase()) into the tag name iterator chain
for both open and close tags, then drop the subsequent .to_ascii_lowercase()
call. Eliminates a second String allocation and copy per tag in the
HTML tokenizer.
* rustfmt
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>
* Harden against hostile-document DoS and non-web link export
A security pass found several small files that make the app allocate or
compute without bound when opened (GUI, wordcraft-cli, or the MCP server),
and link targets leaving on export without the scheme check the HTML and
Markdown writers already apply. None lets a document run code or read files;
all are denial-of-service or credential-leak-on-click. Each fix bounds an
attacker-controlled size and lands with a regression test, per the
never-crash standard.
- layout: a table repeats its header rows on every page it spills onto, so
many tall header rows paginate to O(headers x pages) placed items. Cap the
total header-row repeats per table (lib.rs). 2 KB docx: 1.1 GB -> 124 MB.
- formats/model, html: an HTML table of many <tr> with large rowspan expanded
to rows x 64 covered cells, and `model::table` pre-allocated a full
rows x cols grid only to clear it. Cap rows at MAX_ROWS before
insert_covered, bound its total work, and build the grid template with one
row. 640 KB html: >1.5 GB -> 214 MB (a plain large table is faster too).
- docx/read/freeform: a custom-geometry Bezier segment took any number of
points, making de Casteljau flattening quadratic. Cap points per segment.
3 KB docx: 12 s -> 0.15 s.
- docbin: footnote/header/text-box story tables and the piece table can name
the same backing text many times. Hold the total characters materialised to
twice the WordDocument stream size (a ceiling no real file reaches, since
each char costs >=1 stream byte). 45 KB doc: >1.2 GB -> 14 MB.
- render, pdf, formats/rtf: document pictures were decoded with no dimension
limit, so a tiny file declaring a huge image could allocate before the size
check. Decode through image::Limits with a side cap first.
- doc/link, formats/rtf, odt, latex: route exported hyperlink targets through
is_safe_href (as HTML/Markdown already do), and extend it to reject
protocol-relative (//host) and UNC (\host) references. Keeps file:,
javascript:, ms-msdt: and UNC targets from being written into exported RTF,
ODT, LaTeX and PDF where a reader would follow them (UNC leaks NTLM on
Windows).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Align picture-decode limits with the bounds already in force
Review of the previous commit: `image::load_from_memory` and
`load_from_memory_with_format` already applied `Limits::default()`, whose
512 MB allocation cap bounded every picture decode. The PDF change had set
`max_alloc = None`, loosening that; restore the default. The render and RTF
side caps (16 384 px) newly rejected wide-but-short pictures that fit in
memory, so render now uses the pixmap's own `u16` side limit (what
`premul_pixmap` enforced after decoding, applied before it instead) and the
RTF re-encode keeps only the explicit allocation cap. Net effect: rejection
moves to header time with no change to the sizes accepted. Also fix a
comment: the header-row repeat cap is per table, not per document.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* #1381: WIP
* #1381: HTML import: bold <th>, cell text-align, region paper for HTML/MD/TXT
Header cells are bold unless their own CSS says otherwise. text-align on a
td/th, or inherited from its tr/row group/table, aligns the cell's
paragraphs; a paragraph's own alignment wins, and a table's align attribute
(which places the table) no longer counts. Files that give no page size
(HTML, Markdown, plain text) open on the region's paper, as a new blank
document does: file.open uses the session's region, wordcraft-cli reads the
system region the way the app does (Region::from_locale_vars, shared with
ui-egui). Tables size to that page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #1381: the CLI uses the system region everywhere; help mentions WORDCRAFT_REGION
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>
* #816: copied equations reach other programs as MathML
A copy or cut whose selection holds an equation now also puts HTML on the
system clipboard, with each equation as Presentation MathML (wrapped in
<semantics> with the linear format as an application/x-unicodemath
annotation), so Word, LibreOffice, Google Docs and browsers paste a
formatted equation instead of `y=2x_s`.
- formats: new mathml module, a writer from the equation tree (runs,
fractions, scripts and pre-scripts, radicals, n-ary operators, delimiters,
functions, limits, accents, bars, border boxes, group characters, equation
arrays, matrices, phantoms) and a bounded reader (our annotation, else the
presentation elements, else a TeX annotation). HTML export writes
equations as MathML; HTML import (open and paste) reads <math> back.
- engine: edit.copy/edit.cut (and column copy) return `html` and keep
Session::clipboard_html when the fragment holds an equation.
- ui-egui: Services::set_clipboard_html; copy sites put HTML with the text
through it, falling back to egui's text copy.
- wordcraft: arboard set().html() with the text alternative; on Wayland
without data-control the text-only path is kept.
OMML on the clipboard (Word's "Office Open XML") needs custom clipboard
formats arboard can't set; left as a follow-up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* #816: honest clipboard docs; MathML reader fixes (token cap, function names, column vectors)
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>
* Open RTF in its code page, and Word/RTF files whose extension is wrong
RTF import read every 8-bit byte as Windows-1252, so Cyrillic (and any
other non-Western) RTF came out as mojibake. Bytes now decode in the
current font's code page (\fcharset, \cpg), else the default font's,
else the document's (\ansicpg), for the single-byte Windows code pages
874 and 1250-1258.
A .doc, .docx or .rtf whose content is another of these formats now opens
by its signature, as Word does: RTF saved as .doc (hh.ru résumés and
accounting exports are commonly saved that way), a .docx renamed .doc, a
Word 97-2003 file renamed .docx.
* RTF: read the page size and margins, lay tables out on their cell edges
RTF documents opened on US Letter with 1" margins whatever they said, and
every table got equal columns and a full grid. Russian accounting and HR
exports (A4, 2 cm margins, borderless layout tables whose rows have
different \cellx edges) came out about twice as long as in Word.
- \paperw/\paperh/\margl…\margb (and the …sxn section forms), \headery,
\footery and \landscape set the document's section; out-of-range values
keep Word's defaults. Tables size to that page.
- A table's column grid is the union of its rows' \cellx edges; each cell
spans the grid columns between its left and right edges, as Word does.
- A table with no left/right cell borders is laid out without the grid
style (\clbrdrb rules alone don't make it ruled; \brdrnone is no border).
On 44 RTF files (mostly .doc-named exports) checked against Word's page
counts: total page-count difference 198 -> 30, exact matches 3 -> 17, none
worse.
* RTF: paragraph spacing and indents, RTF defaults, the default font
- \sb, \sa, \sl/\slmult (multiple, at least, exactly), \li/\lin, \ri/\rin
and \fi are read onto each paragraph (new model::Spacing); \pard resets
them.
- The document's paragraph defaults are RTF's (single spacing, no space
around paragraphs) instead of a new document's 1.15 and 8 pt after.
- Text that never selects a font is in the default font (\deff, font 0
when the file names none), as Word shows it.
Against Word's page counts on 251 real documents (44 RTF, mostly .doc-named,
plus .docx): exact matches 212 -> 216, within 10% 248 -> 249; a 68-page
Russian federal law RTF goes from 89 to 73 pages (the rest is the Word
installation's own default margins, which the file doesn't name).
* RTF: import headers and footers
\header, \headerr, \headerl, \headerf and the \footer… forms were skipped,
so letterheads, logos and "page N" lines were lost. Each group is now read
into its own output (the body's paragraph, table and row state is set aside
and put back when the group closes), built into the document with its
pictures (new model::part_blocks), and attached to the section as the
default, first-page or even-page header or footer; \titlepg sets the
separate first page. A header left open by a truncated file still ends.
40 of 41 RTF files in the corpus have headers or footers; against Word's
page counts exact matches go 216 -> 218 of 251, none worse.
* RTF: cell and paragraph borders
Tables either got a full grid (any left/right cell border) or none, and
paragraph borders were kept only on empty paragraphs (as a rule), so the
rules under letterheads and section titles were lost.
- Cells: \clbrdrt/l/b/r with their line style (\brdrs, \brdrdb, \brdrdot,
\brdrdash, \brdrth, \brdrtriple, \brdrwavy…, \brdrnone), \brdrw width and
\brdrcf colour become the cell's own borders (model::Cell::borders); RTF
tables get no grid style, so only the borders the file names are drawn,
as in Word.
- Paragraphs: \brdrt/b/l/r likewise become the paragraph's borders
(model::Para::borders), reset by \pard; an empty paragraph with only a
bottom border stays a rule.
40 of 41 RTF files in the corpus name cell borders; page counts against
Word are unchanged (218 of 251 exact).
---------
Co-authored-by: Магда М.Г. <mail@mgmagda.ru>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- `ui.render` and `ui.screenshot` without a `path` return the PNG as base64 `png` in the
reply instead of writing a fixed name into the shared temporary folder, where another
user could have planted a link to one of this user's files; the MCP bridge's
`render_page` and `screenshot` use that and no longer round-trip through
`temp_dir()/wordcraft-mcp-*.png`. A given `path` still works as before (#678).
- One allow-list, `wordcraft_doc::link`: `is_web_or_mail_link` for Ctrl/⌘-click and
`is_safe_href` (web, mail, `#bookmark`, relative path; schemes read the way browsers
do, so `java\nscript:` is a script) for the HTML and Markdown exporters and the HTML
importer. Scripts and other schemes export as plain text (#679).
- Tests: the allow-list, exporters with hostile links, the MCP bridge against a fake app
(no `path` ever asked for), the control channel's in-reply PNGs.
Co-authored-by: petycomppety <30414255+petycomppety@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
- TXT: the engine opens and saves .txt through wordcraft-formats (BOM/unmarked
UTF-16 sniffing, Windows-1252 fallback; list labels and a tab on export)
instead of its own narrower decoder and body plain text.
- RTF: \uN and \ucN are handled in document info, bookmark names and field
instructions, not only in body text.
- ODT: common paragraph styles (and common parents of automatic styles) give
their text formatting, except the style that sets the paragraph kind;
table-cell-properties fo:background-color becomes cell shading.
- HTML: tables without style or visible borders export without cell borders;
white-space:pre paragraphs keep leading/trailing spaces.
- LaTeX: brace groups restore the paragraph alignment set inside them.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A few hundred KB of `*[`, `, `<` (for `>`) and `&` (for `;`). Bracket and destination scans now
share a per-paragraph look-ahead budget, tags are searched within 4 KB and entity names
within 33 bytes.
- HTML `unescape` searched the whole remaining text for `;` after every stray `&`; it now
looks only at the first 33 bytes.
- The Markdown/HTML-to-document step appended every inline through `insert_text`, which
rescans all earlier runs, so a paragraph with thousands of runs took quadratic time on
its own. It now appends runs directly and normalises once.
Ordinary links, images, tags and entities read as before.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
A chart or SmartArt diagram in a DOCX was dropped on import: it took no
room, so the text after it moved up (five of the six chart documents in
docxide-pdf's fixtures came out a page short), and nothing was drawn.
They are now an InlineObject::Graphic: it takes its room, floats and
wraps like a picture, and draws a list of neutral items (shapes, paths,
one-line texts, pictures) that layout scales to the object's box.
- Charts (read/chart.rs) are drawn from the values the chart part
caches: clustered, stacked and 100% bar and column charts, line, area,
pie, doughnut and scatter charts, with title, legend, gridlines, axis
labels and Excel number and date formats.
- SmartArt (read/diagram.rs) is drawn from the drawing Word stores
beside the diagram: preset shapes, theme colours, text and pictures.
- Theme colour handling is shared (read/drawing_color.rs).
- Each chart or diagram part is read once, only through a relationship
of its type, under a per-file budget of items.
- Tagged PDF tags them as figures with their alternative text, and the
accessibility check reports charts without one.
Not yet: charts and diagrams aren't written back when saving a DOCX
(they weren't before either), so the editor doesn't let you move or
resize them. Radar, stock, 3-D and combination charts draw their first
supported chart type only; SmartArt pictures aren't clipped to their
shape.
docxide-pdf bench (365 documents, vs main + #135): page counts right
320 -> 326 (all five chart documents and one SmartArt document), Jaccard
passes 208 -> 215, SSIM passes 76 -> 79; eight documents better, none
worse.
Co-authored-by: Oliver Zieschang <4963183+naxvog@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* HTML import: pictures referenced by a relative path are embedded (#98)
An img whose src was a path next to the HTML file was dropped, leaving
only its alt text; only data: URIs loaded. The HTML importer now takes
an optional image loader (the formats crate stays free of file access
and wasm-safe), and opening an HTML file from disk loads relative
sources from the file's folder. URLs and absolute paths are not
followed, and files that aren't pictures are ignored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* HTML import: relative pictures stay in the folder, read only regular files, within a budget (#98)
Review follow-up for #113. Pictures named by a relative path are now
loaded by LocalImages:
- Only regular files are read. The path is checked before opening (a
named pipe would block the open), the opened handle is checked again,
and the read goes through take(limit + 1), so the per-picture limit
holds even for files that report a length of 0, like /dev/zero.
- The path is canonicalized and must stay inside the HTML file's
folder, so `..` and symlinks can't reach other files.
- Each file is read once and shared between references, and every
reference counts against a 1 GB budget for the document, so one large
picture referenced many times can't make the import read or copy
without bound. Bytes read from oversized files count too.
The image loader now returns Arc<Vec<u8>>, and make_img takes either a
Vec or an Arc.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* 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>
* HTML import: keep body text when </head> is omitted
Close an open head implicitly on body-content start tags, before skipped raw-text handling. Preserve head metadata and title capture while allowing body paragraphs and inline text to import.
Co-Authored-By: Codex <noreply@openai.com>
* RTF import: read pictures inside shppict wrappers
Recognize shppict as a transparent destination so nested pict groups are decoded instead of inheriting Skip. Continue skipping nonshppict legacy duplicates and pictures under other skipped destinations.
Co-Authored-By: Codex <noreply@openai.com>
---------
Co-authored-by: Codex <noreply@openai.com>