Layout › Breaks (next page, continuous, even, odd) deleted the selection before splitting, so with "one" selected the first section came out empty. The break now goes just before the selection, which keeps its text and direction, as in Word. 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>
This commit is contained in:
parent
1695705241
commit
35b3600ea5
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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<String> {
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
Loading…
Reference in New Issue