diff --git a/ROADMAP.md b/ROADMAP.md index 86f8421..fd1b073 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-11 · **Change:** 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-11 · **Change:** trivial (progress log: section break over a selection, #852; 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 WordCraft aims for complete parity with Microsoft Word, then goes further on speed, openness and agent control. This page is the summary; the assessment is @@ -166,6 +166,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | 2026-10-12 | Keyboard context menu (#821): Shift+F10 opens the page's right-click menu below the caret; `ui.contextMenu` | | 2026-10-12 | Scrolling a document with one very large table stays smooth (#811): pages are fingerprinted for the texture cache once per layout and only when shown (not every page every frame), the markup-area check is cached per revision, and textures far from the view are dropped. The issue's 1,096-page table: ~600 ms → ~2.4 ms idle frames, ~590 ms → ~4.5 ms scroll frames | | 2026-10-12 | Search (#819): Alt+Q, or Alt then Q, opens the Search box from anywhere with nothing typed into it; queries match every word; `tools.search` for agents | +| 2026-10-12 | Section breaks (#852): Layout › Breaks with text selected puts the break before the selection and keeps the text, as Word does; it used to delete it, leaving the first section empty | | 2026-10-12 | Find, Replace and Go To like Word (#712): Ctrl+F types into the Navigation pane's search, one tabbed Find and Replace dialog, Go To by page (absolute or relative) and bookmark; `edit.findReplace`, `edit.goto` `pageBy` | | 2026-10-11 | Hide White Space (#645): `view.hideWhiteSpace` (View › Show checkbox, or double-click the gap between pages) collapses margins, headers and footers between pages to a thin line in Print Layout; screen only, page count unchanged | | 2026-10-11 | Fonts: Palatino Linotype and Book Antiqua fall back to Palatino (macOS) or TeX Gyre Pagella / P052 / URW Palladio (Linux) before generic serif (#670) | @@ -272,6 +273,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | 2026-10-12 | trivial | Progress log: Start screen at launch (#766) | | 2026-10-12 | trivial | Progress log: Alt+Q command search (#819); log table header fixed | | 2026-10-12 | trivial | Progress log: ribbon groups fit above their labels | +| 2026-10-12 | trivial | Progress log: section break keeps the selected text (#852) | | 2026-10-12 | trivial | Progress log: save prompt from the keyboard (#861) | | 2026-10-12 | trivial | Progress log: craft-fonts Simplified Chinese in documents | | 2026-10-11 | trivial | Progress log and shipped languages: Turkish interface completed, 2,325 entries (#774) | diff --git a/crates/engine/src/cmd/page.rs b/crates/engine/src/cmd/page.rs index c98be2a..2e9b7f4 100644 --- a/crates/engine/src/cmd/page.rs +++ b/crates/engine/src/cmd/page.rs @@ -480,14 +480,24 @@ fn breaks(s: &mut Session, v: &Value) -> CmdResult { "oddPage" => SectionStart::OddPage, x => return Err(CmdError::Params(format!("unknown break `{x}`"))), }; - if s.sel.focus.story != wordcraft_doc::StoryRef::Body || s.sel.focus.path.depth() > 0 { + // A section break goes just before the selection and never replaces it, as in Word (#852: + // selected text was deleted, so the first section lost its text). + let old = s.sel.clone(); + let (at, end) = old.ordered(); + if at.story != wordcraft_doc::StoryRef::Body || at.path.depth() > 0 { return Err(CmdError::Failed("section breaks go in the main text".into())); } // Section break: the paragraph before the caret ends a section with the current props; the // new section (after) takes them too and starts as requested. - let at = super::delete_selection(s)?; + s.sel = Selection::caret(at.clone()); let cur = sect(s); - let new = s.doc.split_paragraph(&at)?; + let new = match s.doc.split_paragraph(&at) { + Ok(p) => p, + Err(e) => { + s.sel = old; + return Err(e.into()); + } + }; let ended = at.path.clone(); let para = s.doc.para_mut(at.story, &ended)?; let mut props = cur; @@ -496,11 +506,32 @@ fn breaks(s: &mut Session, v: &Value) -> CmdResult { para.section = Some(Box::new(props.clone())); props.start = start; *s.doc.section_mut(next_block) = props; - s.sel = Selection::caret(Pos { off: 0, ..new }); + // The selection keeps its text, which now starts the new section. + let first = Pos { off: 0, ..new }; + let last = shifted_by_split(&end, &at, &first); + s.sel = if old.anchor <= old.focus { Selection { anchor: first, focus: last } } else { Selection { anchor: last, focus: first } }; let _ = Block::Para; sel_result(s) } +/// Where `p` lands after the top-level body paragraph at `at` was split there, the second half +/// starting at `new`: later text in that paragraph moves into the new one, later blocks move down. +fn shifted_by_split(p: &Pos, at: &Pos, new: &Pos) -> Pos { + if p.story != at.story { + return p.clone(); + } + if p.path == at.path { + return if p.off >= at.off { Pos { off: p.off.saturating_sub(at.off), ..new.clone() } } else { p.clone() }; + } + let mut q = p.clone(); + if let (Some(first), Some(i)) = (q.path.0.first_mut(), at.path.0.first()) + && *first > *i + { + *first = first.saturating_add(1); + } + q +} + #[cfg(test)] mod tests { use super::*; @@ -532,6 +563,56 @@ mod tests { assert_eq!(sect(&s).text_direction, TextDirection::Down); } + /// #852: a continuous section break with text selected goes before the selection and keeps + /// it, as in Word; it used to delete the text, so the first section came out empty. Saved and + /// opened again, both sections' text lays out on page 1, the second below the first. + #[test] + fn section_break_keeps_the_selected_text() { + let mut s = Session::new(wordcraft_doc::Document::from_text("one\ntwo")); + s.run("select.text", &json!({"text": "one"})).unwrap(); + s.run("layout.break", &json!({"kind": "continuous"})).unwrap(); + assert_eq!(s.selected_text(), "one"); + assert_eq!(s.doc.sections().len(), 2); + let texts = |d: &wordcraft_doc::Document| -> Vec { + d.body.iter().filter_map(|b| if let Block::Para(p) = &**b { Some(p.plain_text().to_string()) } else { None }).collect() + }; + assert_eq!(texts(&s.doc), ["", "one", "two"]); + // A backward selection in the middle of a paragraph keeps its text and direction too. + let mut s = Session::new(wordcraft_doc::Document::from_text("alpha beta\ngamma")); + let p = |off| Pos { story: StoryRef::Body, path: wordcraft_doc::Path::top(0), off }; + s.sel = Selection { anchor: p(10), focus: p(6) }; + s.run("layout.break", &json!({"kind": "continuous"})).unwrap(); + assert_eq!(s.selected_text(), "beta"); + assert!(s.sel.anchor > s.sel.focus); + assert_eq!(texts(&s.doc), ["alpha ", "beta", "gamma"]); + + // The issue's document: "one", a continuous break, "two" — both on page 1 after a round trip. + let mut s = Session::new(wordcraft_doc::Document::from_text("one\ntwo")); + s.run("select.text", &json!({"text": "one"})).unwrap(); + s.run("caret.end", &json!({})).unwrap(); + s.run("layout.break", &json!({"kind": "continuous"})).unwrap(); + let back = wordcraft_docx::read(&wordcraft_docx::write(&s.doc).unwrap()).unwrap(); + assert_eq!(back.sections().len(), 2); + assert_eq!(back.last_section.start, SectionStart::Continuous); + let mut s = Session::new(back); + let l = s.layout(); + assert_eq!(l.pages.len(), 1); + let mut lines: Vec<(String, f32)> = l.pages[0] + .items + .iter() + .filter_map(|it| match it { + crate::layout::Placed::Lines { path, y, .. } => { + let Some(Block::Para(p)) = s.doc.body.get(path.last()).map(|b| &**b) else { return None }; + (!p.plain_text().is_empty()).then(|| (p.plain_text().to_string(), *y)) + } + _ => None, + }) + .collect(); + lines.sort_by(|a, b| a.1.total_cmp(&b.1)); + let order: Vec<&str> = lines.iter().map(|(t, _)| t.as_str()).collect(); + assert_eq!(order, ["one", "two"]); + } + /// More Columns: unequal widths with a line between, as one undo step. #[test] fn unequal_columns_with_a_line_between() { diff --git a/docs/layout-parity.md b/docs/layout-parity.md index 187f7e9..5e705c8 100644 --- a/docs/layout-parity.md +++ b/docs/layout-parity.md @@ -1,6 +1,6 @@ # Layout and pagination parity with Microsoft Word -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** trivial (large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** trivial (section break over a selection keeps the text, #852; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 A document only "opens correctly" if its pages break where Word breaks them: the same lines, the same page count, footnotes and pictures on the same pages. This checklist covers line breaking, @@ -33,7 +33,7 @@ font can't match Word's metrics. | Hyphenation (automatic, zone from the document, words in capitals, soft hyphens, consecutive limit, Manual Hyphenation, #407) | 🟡 | English patterns only; other languages hyphenate wrongly or not at all | in localization | | Drop caps | ✅ | 2026-10-06 | — | | Columns, column breaks, separators, balancing | ✅ | Columns balance on a section's last page when the next section starts continuous (also at the document end, which Word only balances when the document ends with a continuous break); not after next-page/even/odd breaks or on a page with a column break; keep-together, keep-with-next, widow/orphan control and unsplittable rows can leave them slightly uneven (#350) | — | -| Section breaks (next page, continuous, even, odd), different first page, odd/even headers | ✅ | Section breaks don't show in Draft view (#42) | 1–2 | +| Section breaks (next page, continuous, even, odd), different first page, odd/even headers | ✅ | Section breaks don't show in Draft view (#42). A break inserted with text selected goes before the selection and keeps it, as in Word (#852) | 1–2 | | List labels: number formats (decimal, letters, roman, ordinal text, Chinese/Japanese/Korean counting and legal, 一〇一 digits, 甲乙丙, ①…⑳, full-width), `%n` text such as `第%1章` / `(%1)`, legal numbering | ✅ | `doc::cjk_numbers` tests, `layout::tests::chinese_counting_list_labels_and_their_font_runs`; a label mixing faces (digit + CJK comma) is drawn run by run (#534). Hebrew, Arabic, Hindi, Thai, Vietnamese and Russian formats show decimal | — | | Body top below a tall header, footer pushes body up | ✅ | #138; a footer taller than the bottom margin moves the body bottom up (#453), and a header or footer linked to the previous section takes its room too (#454) | — | | Footnotes at the page bottom, endnotes at the end, note numbering | 🟡 | A long note continues onto the next page at a line boundary (its first line stays with the reference), first in that page's note area under a full-width continuation separator (#352); the document's own separator, continuation separator and continuation notice are drawn (the notice under a note that continues); a long endnote continues on the next column or page at a line boundary (#456), and the caret finds note text on its continuation page (#457); per-section restart partial | 1–3 | @@ -90,6 +90,7 @@ font can't match Word's metrics. | 2026-10-12 | trivial | Space before at the top of a page after page breaks, measured in Word for Windows in compatibility modes 12 and 15 (32 cases) | | 2026-10-12 | trivial | No automatic (HTML-style) spacing between two list items, of one list or two, at any level; the first item keeps its automatic space before and the last its space after | | 2026-10-12 | trivial | Kerning only where the document asks for it, and automatic (HTML-style) paragraph spacing, both measured in Word for Windows against real .gov documents | +| 2026-10-12 | trivial | A section break inserted over a selection keeps the selected text: the first section no longer comes out empty (#852) | | 2026-10-12 | trivial | Large-table scrolling (#811): per-frame page fingerprinting fixed; measurement added | | 2026-10-11 | trivial | Chinese documents (#534): indents in characters, a page-anchored floating table after a full page moves to the next page, no blank page from a page break at the bottom of a full page | | 2026-10-11 | trivial | East Asian list number formats (一、二、三, 第一章, ①); list labels whose characters come from two fonts draw each part with its own font (the digits of "1、" no longer show as other letters); a list level without indents leaves the label at the paragraph edge (#534) |