diff --git a/ROADMAP.md b/ROADMAP.md
index 07e73837e..0500b70bf 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -2,7 +2,7 @@
**Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (progress log: privacy options on save, #1062; previously trivial: progress log: absolute interface sizes, #822; previously trivial: progress log: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (progress log: privacy options on save, #1062; previously trivial: progress log: equations copy as MathML, #816; previously trivial: progress log: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
> **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below.
@@ -146,6 +146,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| 2026-10-13 | Dragging a Square-wrapped shape or picture on a multi-column page wraps the text of the column it lands in around it, and its old place closes up (#884) |
| 2026-10-13 | Pagination with footnotes follows Word (#1116): a paragraph ending on a page with footnotes keeps its space after above the separator, otherwise its last lines move to the next page with the note; pages without notes are unchanged |
| 2026-10-13 | Interface size means what it says (#822): a chosen size is absolute, as in PhotoCraft and PdfCraft, so "100%" is one screen pixel per point even where the system reports 200 % (a bare window manager on X11, where winit guesses the scale from the monitor's DPI). The new default, Auto, keeps the system's display scale and its menu entry shows the size it draws at; sizes run 75–300 %. A saved size from before stays the same if it was the default (now Auto) and becomes absolute otherwise |
+| 2026-10-13 | Copying an equation also puts MathML in HTML on the clipboard beside the linear text like `y=2x_s` (#816; Word/LibreOffice paste not yet verified; OMML is #1297). HTML export writes equations as MathML too, and opening or pasting HTML with MathML gives equations back |
| 2026-10-12 | RTF import now uses the RTF spec's defaults where a file names none (#902): no space before or after paragraphs, single line spacing, and the `\deff` default font, instead of WordCraft's Normal style; RTF files that relied on the old defaults open tighter, like in Word |
| 2026-10-11 | Right-clicking a table cell offers the table's commands, and as in Word they follow the selection: with the caret in a cell Insert, Delete Cells, Split Cells, Text Direction and Table Properties…; with several cells Merge Cells instead of Split; with whole columns or rows Delete Columns or Rows, Merge Cells and Distribute Rows and Columns; with the whole table Delete Table, Distribute, AutoFit and Insert Caption…. A right-click beside selected cells moves the caret there instead of keeping them |
| 2026-10-11 | Drags, Shift+arrows and Shift+click over table cells select whole cells, as in Word: from one cell into another the rectangle between them (bold, alignment and Delete leave the other cells alone), into or out of a table whole rows; extending again goes on from where the selection started |
diff --git a/apps/wordcraft/src/clipboard.rs b/apps/wordcraft/src/clipboard.rs
index ecd284679..1776d8fd0 100644
--- a/apps/wordcraft/src/clipboard.rs
+++ b/apps/wordcraft/src/clipboard.rs
@@ -8,6 +8,11 @@
//! browser or Spectacle never arrived (#833). Compositors without the protocol fall back to X11.
//!
//! Everything read here comes from other programs: sizes are capped as it is read.
+//!
+//! Written here: a copy holding an equation puts HTML (the equation as MathML) beside its text
+//! (#816), for programs that read MathML (pasting into Word and LibreOffice not yet verified). arboard sets HTML with a
+//! text alternative on every desktop platform; formats of other names (Word's "Office Open XML"
+//! with OMML, #1297) it can't, so they aren't offered.
use wordcraft_engine::cmd::paste::MAX_PASTE_BYTES;
use wordcraft_ui_egui::paste_picture::ClipboardPicture;
@@ -16,6 +21,35 @@ fn open() -> Option {
arboard::Clipboard::new().map_err(|e| log::warn!("clipboard: {e}")).ok()
}
+thread_local! {
+ /// The clipboard copies are set through, kept open: on X11 the contents are served from it.
+ static WRITER: std::cell::RefCell
")), vec![(linear.to_string(), display)], "{html}");
+ }
+ }
+
+ #[test]
+ fn foreign_mathml_is_read_from_its_elements() {
+ let cases = [
+ ("y=2xs", "y=2x_s"),
+ ("ab+1", "a/(b+1)"),
+ ("x", "√x"),
+ ("∑k=1nk", "∑_(k=1)^n▒k"),
+ ("ex", "e^x"),
+ // A TeX annotation is used when the presentation is empty.
+ ("\\frac{1}{2}", "1/2"),
+ ];
+ for (html, want) in cases {
+ let got = pasted(html);
+ assert_eq!(got.first().map(|g| g.0.as_str()), Some(want), "{html}");
+ }
+ assert!(pasted("x").first().is_some_and(|g| g.1));
+ // Text after the equation still arrives; an unclosed element ends with the input.
+ let f = crate::html::parse("
x").is_empty());
+ // Hostile nesting is bounded.
+ let deep = format!("{}x{}", "".repeat(5000), "".repeat(5000));
+ assert!(pasted(&deep).len() <= 1);
+ }
+
+ #[test]
+ fn math_over_the_token_cap_is_skipped_to_its_end() {
+ // Past the cap, the rest of the element is dropped, not read as body text.
+ let html = format!("
{}z tail
", "y".repeat(MAX_NODES * 2));
+ let flow = crate::html::parse(&html);
+ let mut text = String::new();
+ for b in &flow.blocks {
+ if let crate::model::FBlock::Para(p) = b {
+ for i in &p.inlines {
+ if let crate::model::Inline::Text(t, _) = i {
+ text.push_str(t);
+ }
+ }
+ }
+ }
+ assert_eq!(text.trim(), "tail");
+ }
+
+ #[test]
+ fn function_names_apply_without_the_invisible_operator() {
+ let cases = [
+ ("sinx", "sin\u{2061}x"),
+ ("logab", "log\u{2061}ab"),
+ ("cos\u{2061}x", "cos\u{2061}x"),
+ ("abx", "abx"),
+ ];
+ for (html, want) in cases {
+ assert_eq!(pasted(html).first().map(|g| g.0.as_str()), Some(want), "{html}");
+ }
+ let tree = to_tree(&El {
+ name: "math".into(),
+ attrs: vec![],
+ children: vec![
+ Item::El(El { name: "mi".into(), attrs: vec![], children: vec![Item::Text("sin".into())] }),
+ Item::El(El { name: "mi".into(), attrs: vec![], children: vec![Item::Text("x".into())] }),
+ ],
+ });
+ assert!(matches!(tree.as_slice(), [MNode::Func { .. }]), "{tree:?}");
+ }
+
+ #[test]
+ fn one_column_tables_are_column_vectors_unless_marked() {
+ let tree = |html: &str| {
+ let linear = pasted(html).first().map(|g| g.0.clone()).unwrap_or_default();
+ wordcraft_doc::math::parse_linear(&linear)
+ };
+ let vector = tree("ab");
+ assert!(matches!(vector.as_slice(), [MNode::Matrix { rows, .. }] if rows.len() == 2), "{vector:?}");
+ for marked in [
+ "ab",
+ "a=1b",
+ ] {
+ let t = tree(marked);
+ assert!(matches!(t.as_slice(), [MNode::EqArr { rows }] if rows.len() == 2), "{marked}: {t:?}");
+ }
+ // Our own equation arrays read back as equation arrays from their elements alone.
+ let ml = to_mathml(&[MNode::EqArr { rows: vec![parse_linear("x=1"), parse_linear("y=2")] }], false, "");
+ let ml = ml.replace("application/x-unicodemath", "x-ignored");
+ let t = tree(&ml);
+ assert!(matches!(t.as_slice(), [MNode::EqArr { .. }]), "{ml}: {t:?}");
+ }
+
+ #[test]
+ fn deep_nesting_is_bounded() {
+ let mut n = parse_linear("x");
+ for _ in 0..500 {
+ n = vec![MNode::Frac { kind: FracKind::Bar, num: n, den: parse_linear("y") }];
+ }
+ let m = to_mathml(&n, false, "z");
+ assert!(m.ends_with(""));
+ }
+}
diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs
index 10b569dac..49860fe0a 100644
--- a/crates/ui-egui/src/canvas.rs
+++ b/crates/ui-egui/src/canvas.rs
@@ -2494,10 +2494,8 @@ fn context_menu(app: &mut WordApp, ui: &mut Ui) {
}
item(ui, app, "Cut", "edit.cut", json!({}));
if ui.add(egui::Button::new(tl!("Copy")).shortcut_text(crate::widgets::shortcut_text(app, "edit.copy"))).clicked() {
- if let Ok(r) = app.run("edit.copy", json!({}))
- && let Some(t) = r.get("text").and_then(|t| t.as_str())
- {
- ui.ctx().copy_text(t.to_string());
+ if let Ok(r) = app.run("edit.copy", json!({})) {
+ crate::system_clipboard::put_result(app, ui.ctx(), &r);
}
ui.close();
}
diff --git a/crates/ui-egui/src/keys.rs b/crates/ui-egui/src/keys.rs
index 15ae9d195..cde1ffd7c 100644
--- a/crates/ui-egui/src/keys.rs
+++ b/crates/ui-egui/src/keys.rs
@@ -315,11 +315,8 @@ pub fn canvas_events(app: &mut WordApp, ctx: &egui::Context) {
}
egui::Event::Copy | egui::Event::Cut => {
let id = if matches!(e, egui::Event::Cut) { "edit.cut" } else { "edit.copy" };
- if let Ok(r) = app.run(id, json!({}))
- && let Some(t) = r.get("text").and_then(Value::as_str)
- && !t.is_empty()
- {
- ctx.copy_text(t.to_string());
+ if let Ok(r) = app.run(id, json!({})) {
+ crate::system_clipboard::put_result(app, ctx, &r);
}
}
egui::Event::Key { key, pressed: true, modifiers, .. } => {
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
index 2d4a71ed3..af0385e80 100644
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -131,6 +131,10 @@ pub struct Services {
/// Desktop: the HTML on the system clipboard — another program's formatted copy (a Word
/// document, a web page) — if any. Paste keeps its formatting (#687); see [`system_clipboard`].
pub clipboard_html: Option Option>>,
+ /// Desktop: put HTML on the system clipboard with its plain-text alternative (html, text), for
+ /// a copy holding an equation (as MathML, #816). `true` when both are there; otherwise the
+ /// copy's text goes through egui as usual.
+ pub set_clipboard_html: Option bool>>,
/// Desktop: the RTF on the system clipboard, where the platform offers it (#687).
pub clipboard_rtf: Option Option>>,
/// Desktop: the names of the formats on the system clipboard (MIME types, Windows format
diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs
index 6eae2b71f..b52d210b2 100644
--- a/crates/ui-egui/src/ribbon.rs
+++ b/crates/ui-egui/src/ribbon.rs
@@ -528,7 +528,7 @@ fn home(app: &mut WordApp, ui: &mut Ui) {
small(ui, app, "cut", Some("Cut"), "Cut", "edit.cut", json!({}), false);
let r = small(ui, app, "copy", Some("Copy"), "Copy", "edit.copy", json!({}), false);
if r.clicked() {
- ui.ctx().copy_text(app.session.clipboard_text.clone());
+ crate::system_clipboard::put(app, ui.ctx(), &app.session.clipboard_text, &app.session.clipboard_html);
}
let active = app.session.painter.is_some();
small(ui, app, "painter", Some("Format"), "Format Painter", "edit.formatPainter", json!({}), active);
diff --git a/crates/ui-egui/src/system_clipboard.rs b/crates/ui-egui/src/system_clipboard.rs
index e11436afd..be77f80ff 100644
--- a/crates/ui-egui/src/system_clipboard.rs
+++ b/crates/ui-egui/src/system_clipboard.rs
@@ -67,6 +67,25 @@ pub(crate) fn fill(app: &mut WordApp, id: &str, params: &mut Value) -> Option bool {
let n = name.trim();
@@ -221,4 +240,33 @@ mod tests {
let e = app(unreadable).run("edit.paste", json!({})).unwrap_err();
assert!(e.contains("couldn't be read") && e.contains("isn't supported"), "{e}");
}
+
+ #[test]
+ fn copying_an_equation_puts_its_html_on_the_system_clipboard() {
+ // #816: the host gets HTML with the equation as MathML beside the text.
+ let got = std::rc::Rc::new(std::cell::RefCell::new(Vec::<(String, String)>::new()));
+ let sink = got.clone();
+ let mut a = app(Services {
+ set_clipboard_html: Some(Box::new(move |h, t| {
+ sink.borrow_mut().push((h.to_string(), t.to_string()));
+ true
+ })),
+ ..Default::default()
+ });
+ a.run("document.setText", json!({"text": "Speed "})).unwrap();
+ a.run("caret.docEnd", json!({})).unwrap();
+ a.run("insert.equation", json!({"linear": "y=2x_s", "edit": false})).unwrap();
+ a.run("select.all", json!({})).unwrap();
+ let ctx = egui::Context::default();
+ let r = a.run("edit.copy", json!({})).unwrap();
+ put_result(&a, &ctx, &r);
+ let got_now = got.borrow().clone();
+ assert_eq!(got_now.len(), 1);
+ assert!(got_now[0].0.contains(" **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** trivial (VML text boxes keep their internal margins, fit to their text and vertical alignment; hidden drawings not shown; previously trivial: Remove personal information and Remove date and time honoured on save, #1062; previously trivial: undrawn chart types show a placeholder, #998; previously trivial: saving Word 2003 XML Document, #1081; previously trivial: direct run-level InkML content parts now survive DOCX saves; previously trivial: symbol-font text exported as Unicode, #776; previously trivial: text-box margins, Resize shape to fit text and freeforms in groups, #743; previously trivial: formatted paste from the system clipboard's HTML and RTF, #687; previously trivial: independent complex-script formatting inheritance and round-trip regression tests; previously trivial: import/export fixes for TXT, RTF, ODT, HTML and LaTeX, #433–#440; previously major: first version; every format Word reads or writes, measured from the readers and writers on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4, plus Word for Windows' extra formats
+> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-13 · **Change:** trivial (VML text boxes keep their internal margins, fit to their text and vertical alignment; hidden drawings not shown; previously trivial: equations as MathML in HTML export, copy and paste, #816; previously trivial: symbol-font text exported as Unicode, #776; previously trivial: text-box margins, Resize shape to fit text and freeforms in groups, #743; previously trivial: formatted paste from the system clipboard's HTML and RTF, #687; previously trivial: independent complex-script formatting inheritance and round-trip regression tests; previously trivial: import/export fixes for TXT, RTF, ODT, HTML and LaTeX, #433–#440; previously major: first version; every format Word reads or writes, measured from the readers and writers on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4, plus Word for Windows' extra formats
Every format Word opens or saves, what WordCraft does with it, and how it's tested. Fidelity is the
share of a typical real-world file's content and formatting that survives (read: shows correctly;
@@ -28,7 +28,7 @@ Weights are the share of a Word user's file traffic (estimate). Read/write: ✅
| `.rtf` Rich Text Format | 3% | R/W | 🟡 | 🟡 | ~50% | 10–15 | `crates/formats/src/tests.rs` | ~1,500 lines: text, character/paragraph formatting, styles, lists, tables, pictures (`\shppict`, #154), fields, bookmarks. 8-bit text in the document's code page (`\ansicpg`) or the current font's (`\fcharset`, `\cpg`): Cyrillic, Central European, Greek, Turkish, Hebrew, Arabic, Baltic, Vietnamese, Thai (was Windows-1252 only). Pasted from the system clipboard where the platform offers it (Windows, Wayland) when there is no HTML (#687). Thin: comments, notes, revisions, sections, RTL |
| `.odt` OpenDocument Text | 2% | R/W | 🟡 | 🟡 | ~50% | 8–12 | `crates/formats/src/tests.rs` | Styles, lists (#87), tables, pictures, headers/footers, page breaks (#86), page size, orientation and margins (#810). No footnotes, comments, tracked changes, fields or RTL |
| `.txt` Plain text | 2% | R/W | ✅ | ✅ | ~95% | 1–2 | unit tests | Encoding detection. Files with other extensions (`.m3u`/`.m3u8` playlists, logs, config files, no extension) open as text when they are text, Word and RTF files by their signature; binary files are refused (#569). A Word or RTF extension that contradicts the content also opens by signature, as in Word: RTF saved as `.doc` (hh.ru résumés, accounting exports), a `.docx` renamed `.doc`, a Word 97-2003 file renamed `.docx`. Word's encoding choice dialog and line-ending options missing; such files save only through Save As |
-| `.htm/.html` Web Page and Web Page, Filtered | 1% | R/W | 🟡 | 🟡 | ~60% | 6–10 | `crates/formats/src/tests.rs` | Tables (#102), relative pictures embedded (#113), missing `` tolerated (#154). No Word "Web Page" (with `_files` folder) round-trip. The same reader takes HTML from the system clipboard: Paste keeps another program's formatting (a Word document, a web page; Windows' "HTML Format" header understood), Merge Formatting keeps its emphasis (#687) |
+| `.htm/.html` Web Page and Web Page, Filtered | 1% | R/W | 🟡 | 🟡 | ~60% | 6–10 | `crates/formats/src/tests.rs` | Tables (#102), relative pictures embedded (#113), missing `` tolerated (#154). No Word "Web Page" (with `_files` folder) round-trip. The same reader takes HTML from the system clipboard: Paste keeps another program's formatting (a Word document, a web page; Windows' "HTML Format" header understood), Merge Formatting keeps its emphasis (#687). Equations are written as MathML (with the linear format as a UnicodeMath annotation) and read back from MathML: our annotation, else the presentation elements, else a TeX annotation (#816) |
| `.mht/.mhtml` Single File Web Page | <1% | R/W | ❌ | ❌ | 0% | 3–5 | — | |
| `.xml` Word XML Document (Flat OPC) | <1% | R/W | ✅ | ✅ | ~65% | — | `crates/docx/tests/word_xml.rs`, `roundtrip.rs` (`flat_opc_round_trip_equals_docx`) | The DOCX package in one XML file, read and written by the DOCX code (#548); File › Export › Word XML Document. `.xml` files are told apart by their root element |
| `.xml` Word 2003 XML (WordprocessingML 2003) | <1% | R/W | 🟡 | 🟡 | ~55% | 1–2 | `crates/docx/tests/word_xml.rs` | Opened by rewriting it as a package for the DOCX reader (#548): styles, default fonts, lists, tables, sections, headers/footers, footnotes/endnotes, comments, bookmarks, tracked insertions/deletions, hyperlinks, `w:binData` pictures, document properties. Not read: formatting changes, `w:docPr` settings, smart tags. Written (#1081) by rewriting the DOCX writer's package into the 2003 vocabulary (`crates/docx/src/wordml2003_write.rs`, `crates/docx/tests/wordml2003_write.rs`): core properties, styles (document defaults folded into Normal), lists, text and paragraph/character formatting, tables, hyperlinks, footnotes/endnotes inline, headers/footers in the section properties, bookmarks, comments and tracked insertions/deletions as `aml:annotation`, section page setup, and PNG/JPEG/GIF/BMP/EMF/WMF pictures as `w:pict` with `w:binData`. **Dropped on write:** shapes, text boxes, groups, charts, SmartArt, ink, equations, OLE objects, picture crop, rotation and floating position (pictures become inline), content-control wrappers (their content stays), formatting-change tracking, theme colours/fonts, custom XML, macros, even/odd header setting. File › Export › Word 2003 XML Document, `file.save`/`file.saveAs` with `format: "wordml2003"`, `wordcraft-cli convert … --format wordml2003`; a plain save over a 2003 file keeps that format. Never opened in Word yet (written from the published schemas; needs a tester) |
@@ -166,6 +166,7 @@ raise readiness estimates. No Word-derived files are fixtures.
| 2026-10-12 | trivial | VML text boxes keep their internal margins (`inset`), fit to their text (`mso-fit-shape-to-text`) and vertical alignment (`v-text-anchor`); hidden drawings (`visibility:hidden`, `wp:docPr/@hidden`) are kept but not shown |
| 2026-10-13 | trivial | Remove personal information / Remove date and time settings kept and honoured on save: anonymous annotation authors, no creator or last-modified-by, no annotation dates (#1062) |
| 2026-10-12 | trivial | Direct run-level `w:contentPart` InkML keeps its relationship, content type and bytes through DOCX edits and repeated saves; nested group/canvas ink and native rendering remain missing |
+| 2026-10-13 | trivial | HTML export writes equations as MathML instead of linear text, and HTML import (open and paste) reads `` back into equations; a copy holding an equation puts that HTML on the system clipboard: MathML in HTML on the clipboard (Word/LibreOffice paste not yet verified; OMML is #1297) (#816) |
| 2026-10-12 | trivial | Paste keeps the formatting of HTML and RTF on the system clipboard; Paste Special lists them and pictures (#687, #833) |
| 2026-10-12 | trivial | ODT page size, orientation and margins read back from `styles.xml` on open, so they survive a save and reopen (#810) |
| 2026-10-12 | minor | RTF reads 8-bit text in the document's or the font's code page (`\ansicpg`, `\fcharset`, `\cpg`) instead of always Windows-1252; a `.doc`/`.docx`/`.rtf` whose content is another of these formats opens by signature. Found on a private corpus of real Russian documents: 38 of 44 `.doc` files were RTF, all now open with their Cyrillic text |
diff --git a/docs/gaps.md b/docs/gaps.md
index 90eed0751..a85a8f0ee 100644
--- a/docs/gaps.md
+++ b/docs/gaps.md
@@ -1,6 +1,6 @@
# Where WordCraft falls short of Word
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** trivial (sample-text formulas on Enter, #1067; previously trivial: radar, stock and other undrawn chart types show a placeholder instead of blank space, #998; previously trivial: saving Word 2003 XML, #1081; previously trivial: direct run-level native InkML content parts now survive DOCX saves; previously trivial: the Font list no longer keeps every previewed font in memory, #848; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (sample-text formulas on Enter, #1067; previously trivial: #29: copied equations as MathML, OMML open as #1297, #816; previously trivial: the Font list no longer keeps every previewed font in memory, #848; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
> **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below.
@@ -43,7 +43,7 @@ Native save reliability: synthetic cases on macOS reproduced loss through the pr
| 26 | **Minor formats**: MHT, Works/WordPerfect | | Word XML Document (Flat OPC) opens and saves, Word 2003 XML opens (#548) and saves (#1081, not yet checked in Word) | Rare | 14–26 | [file-format-parity.md](file-format-parity.md) |
| 27 | **View windows**: Side by Side, Synchronous Scrolling, Arrange All, Switch Windows | | [parity-checklist.md](parity-checklist.md) (View 25/29) | Comparing documents | 3–5 | [ui-parity.md](ui-parity.md) |
| 28 | **Dictate** | | `tools.dictate` missing | Dictation users | 8–15 + owner (speech model) | [hardware-parity.md](hardware-parity.md) |
-| 29 | **Equations**: ink equations | | #191 notes; long display equations wrap at operators (#326) | Maths-heavy documents | 2–3 | [layout-parity.md](layout-parity.md) |
+| 29 | **Equations**: ink equations; copied equations as OMML for Word (#816, #1297) | | #191 notes; long display equations wrap at operators (#326); a copy holding an equation puts MathML in HTML on the clipboard (#816; Word/LibreOffice paste not yet verified); only copies containing an equation get HTML, text-only copies stay plain | Maths-heavy documents | 2–3 | [layout-parity.md](layout-parity.md) |
**Beta needs #1–#7** (~150–230 h with the stability and layout work they imply): they are the
blocking gaps, and closing them lifts file formats, layout, stability and UI enough to take the
@@ -63,7 +63,10 @@ The same gaps grouped the way the parity documents are, for agents working in on
styles, and Draw Table and Eraser (#303) landed on 2026-10-10. Paste from other programs keeps
their formatting (system clipboard HTML/RTF), Merge Formatting keeps emphasis, Paste Special lists
what the system clipboard holds, and Linux/Wayland clipboard pictures paste (#687, #833);
- still missing: the Paste Options button after a paste, and RTF on macOS and X11.
+ still missing: the Paste Options button after a paste, and RTF on macOS and X11. Copying an
+ equation puts MathML in HTML on the clipboard (#816; pasting into Word/LibreOffice not yet
+ verified; OMML on the clipboard is #1297); only copies containing an equation get HTML,
+ text-only copies stay plain text.
- **UI/UX gaps:** #7, #18, #24; crop handles on the canvas (rotation handle landed, #332); context menus for objects (the table's landed; it still lacks Border Styles); Linux title bar theming (#78); File ›
Options › Animation effects › Use system setting reads macOS (Reduce motion) and the Linux/BSD
desktops (`apps/wordcraft/src/animations.rs`) but hasn't been tried on a Mac or in a real
@@ -141,6 +144,7 @@ The same gaps grouped the way the parity documents are, for agents working in on
| 2026-10-10 | trivial | #10: Tight and Through wrapping follow the object's outline (#354) |
| 2026-10-10 | trivial | #10: free rotation and flips for pictures, shapes and text boxes, with a rotation handle (#332); text boxes rotate their frame; their text stays upright |
| 2026-10-10 | trivial | #10: Drawing Canvas landed (#344); in-canvas dragging of members remains |
+| 2026-10-13 | trivial | #29: copied equations reach the clipboard as MathML in HTML (#816); OMML for Word stays open (#1297) |
| 2026-10-10 | trivial | #29: long display equations wrap across lines (#326) |
| 2026-10-10 | trivial | #3: Windows lag on GPUs without a DirectX 12 driver fixed with an OpenGL fallback (#316) |
| 2026-10-10 | minor | #8: Polish proofing (spelling, grammar, hyphenation) and a proofing language per run landed; the gap is now the other languages (20–40 h) |
diff --git a/docs/ui-parity.md b/docs/ui-parity.md
index 5d93f1068..b60bbffd7 100644
--- a/docs/ui-parity.md
+++ b/docs/ui-parity.md
@@ -1,6 +1,6 @@
# UI parity with Microsoft Word
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (no document behind the Start screen, #1129; previously trivial: a chosen interface size is absolute and Auto keeps the display scale, #822; previously trivial: selection bar in the left margin, #856; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (no document behind the Start screen, #1129; previously trivial: copied equations reach other programs as MathML, #816; previously trivial: selection bar in the left margin, #856; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
How WordCraft looks and feels next to Word: ribbon, dialogs, panes, keyboard, mouse and touch.
Typing behaviour has its own checklist ([`typing-parity.md`](typing-parity.md)). All assets are our
@@ -49,6 +49,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
| 2026-10-13 | trivial | The caret is black or white by contrast with the paper, page colour, dark page, shading or highlight under it (#299) |
| 2026-10-13 | trivial | Clicks on ribbon and status-bar chrome that run no command button (Styles and style-set gallery tiles, tab names, empty ribbon space, View check boxes, the status bar's view buttons and language menu) no longer take keyboard focus from the page: the caret stays and typing continues, as in Word; text fields such as the font size box still take focus. Tabs, gallery tiles and the status bar's view buttons report their name and state to screen readers (#1093) |
| 2026-10-13 | trivial | Interface size: a chosen size is absolute ("100%" is one screen pixel per point, even where the system reports 200 % under a bare window manager) and the new default, Auto, keeps the display scale and shows the size it draws at; 75–300 %; an old saved size becomes the size it drew at; on the web a chosen size cancels browser zoom, Auto follows it (#822) |
+| 2026-10-13 | trivial | Copy and Cut of a selection holding an equation put HTML with the equation as MathML on the system clipboard beside the text (desktop, through arboard), as MathML in HTML on the clipboard (Word/LibreOffice paste not yet verified; OMML is #1297) (#816). Only copies containing an equation get HTML. Web: text only |
| 2026-10-11 | trivial | Tables on the page: Ctrl shares a column border's change among the columns right of it, the left edge moves a left-aligned table's indent (a centred or right-aligned one keeps its alignment), a border of cells selected across moves only in their rows, Alt shows the column widths on the ruler (#711); 80% → 90% |
| 2026-10-11 | trivial | Rulers: table column and row markers drag their borders (one undo step; from the ruler only the column left of a border changes, with Shift the neighbour makes up the difference), and in a cell the indents and tabs are measured from it (#700) |
| 2026-10-11 | trivial | Table context menu as in Word: its items follow the selection (the caret in a cell, several cells, whole columns or rows, the whole table), adding Delete Cells, Split Cells, Distribute Rows and Columns, AutoFit, Text Direction, Insert Caption… and Table Properties… to Insert, Delete and Merge Cells; a right-click beside selected cells drops them |