diff --git a/ROADMAP.md b/ROADMAP.md index fa6afca0e..a14ef9205 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-18 · **Change:** trivial (Navigation Find exposes Results, #2096; previously trivial: progress log: Columns on selected text, #2024; previously trivial: progress log: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: progress log: Indonesian proofing, #498; previously trivial: progress log: DOCX glossary documents (building blocks); previously trivial: progress log: Read Mode keeps the keyboard and shows whole, unobscured columns, #1772); previously trivial: progress log: font list entries of tall-ascent fonts no longer cut off, #2570); previously trivial: progress log: Split Cells and Convert Text to Table dialogs follow the Word check, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout fills the window width, #1876; previously trivial: progress log: Different First Page headers reachable from page 1, #2826; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: progress log: web preferences kept between visits, #2132; previously trivial: progress log: undrawable private-use list bullets show the level's default bullet, #1683); previously trivial: table style editor formats every part, #714; previously trivial: VML freeforms read and kept on save; previously trivial: progress log: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: progress log: grouped `flowChartOr` shapes draw their circle and crossing spokes; previously trivial: progress log: copies keep their formatting in other programs, #1523; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: 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-18 · **Change:** trivial (numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: progress log: Columns on selected text, #2024; previously trivial: progress log: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: progress log: Indonesian proofing, #498; previously trivial: progress log: DOCX glossary documents (building blocks); previously trivial: progress log: Read Mode keeps the keyboard and shows whole, unobscured columns, #1772); previously trivial: progress log: font list entries of tall-ascent fonts no longer cut off, #2570); previously trivial: progress log: Split Cells and Convert Text to Table dialogs follow the Word check, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout fills the window width, #1876; previously trivial: progress log: Different First Page headers reachable from page 1, #2826; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: progress log: web preferences kept between visits, #2132; previously trivial: progress log: undrawable private-use list bullets show the level's default bullet, #1683); previously trivial: table style editor formats every part, #714; previously trivial: VML freeforms read and kept on save; previously trivial: progress log: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: progress log: grouped `flowChartOr` shapes draw their circle and crossing spokes; previously trivial: progress log: copies keep their formatting in other programs, #1523; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: 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-17 · **Change:** trivial (progress log: pasted blockquotes are indented paragraphs, #1270; previously trivial: progress log: print-area strings translated in every catalog; previously trivial: progress log: comments readable in the dark theme, #2045; previously trivial: progress log: ODT sections, #1379; previously trivial: progress log: table deletes, Change Case and Unlink Fields respect Track Changes and locked content controls, #1329; previously trivial: progress log: rounded rectangle corners, #1476; previously trivial: progress log: Save As type list, #1399; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: 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. @@ -137,6 +137,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm - 2026-10-15: Find opens Navigation on Results; editing the query restores Results after manual Headings/Pages navigation. Typed and selected-text searches retain document contents (#2096). | Date | What landed | +| 2026-10-15 | Numbered headings (#1793): the Multilevel List gallery offers four lists linked to Heading 1–9 (1 / 1.1 / 1.1.1, Article I. / Section 1.01 / (a), I. / A. / 1., Chapter 1; `list.multilevelPreset`). The heading styles carry the numbers, so paragraphs already in a Heading style keep their style and level, any other selected paragraph (Normal, Quote, Title) becomes Heading 1 (checked in Word), a new Heading 2 is numbered 2.1 by itself and body text stays unnumbered; Numbering or typing `1. ` keeps a heading a heading | | 2026-10-15 | Reversing an automatic sentence capital (Undo, or Backspace and retype) adds the word before it to AutoCorrect's First Letter exceptions, as Word does; "Automatically add words to list" switches for First Letter and INitial CAps (#1857). | | 2026-10-15 | Dialog close control: a Windows-style box at the title bar's right end, the whole box clickable, red on hover, in both themes (#1885) | | 2026-10-15 | Font box: a Recently Used Fonts section at the top of the list (the fonts applied lately, most recent first, ten at most, kept between runs), then All Fonts, as in Word (#2068) | @@ -447,6 +448,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | 2026-10-16 | trivial | Progress log: legacy outline, shadow, emboss and imprint text effects drawn (#2534) | | 2026-10-15 | trivial | Progress log: Columns on selected text (#2024) | | 2026-10-15 | trivial | Navigation Find and query edits expose Results (#2096) | +| 2026-10-15 | trivial | Progress log: numbered headings (#1793) | | 2026-10-14 | trivial | Progress log: row table border exceptions | | 2026-10-12 | minor | Czech proofing review: short-word splits rank after deletion, NOSUGGEST excludes suggestions while accepting typed forms, affix comments apply only to headers, ratios keep spaces and repeated-word messages use Czech quotes; synthetic engine end-to-end and latency checks | | 2026-10-12 | minor | Czech spelling from the installed Hunspell dictionary, runtime-table suggestions and generic checks; Czech interface writes cs-CZ documents; no Czech hyphenation patterns bundled; estimates unchanged | diff --git a/crates/engine/src/catalog.rs b/crates/engine/src/catalog.rs index c6a36a378..81f80a8c1 100644 --- a/crates/engine/src/catalog.rs +++ b/crates/engine/src/catalog.rs @@ -82,6 +82,7 @@ Home|Paragraph|Bullets|para.bullets Home|Paragraph|Numbering|para.numbering Home|Paragraph|Multilevel List|para.multilevel Home|Paragraph|Define New Multilevel List|list.define +Home|Paragraph|Multilevel List Library|list.multilevelPreset Home|Paragraph|Change List Level|para.listLevel Home|Paragraph|Restart Numbering|para.restartNumbering Home|Paragraph|Define New Bullet|para.defineBullet diff --git a/crates/engine/src/cmd/lists.rs b/crates/engine/src/cmd/lists.rs index 80ee95e58..671778c1f 100644 --- a/crates/engine/src/cmd/lists.rs +++ b/crates/engine/src/cmd/lists.rs @@ -23,6 +23,9 @@ pub fn specs() -> Vec { CommandSpec::new("list.get", "List Definition", "Home › Paragraph › Multilevel List", get) .params(r#"{"num"?: list id (default: the caret's list)} → {"num", "levels": [as list.define]}"#) .pure(), + CommandSpec::new("list.multilevelPreset", "Multilevel List Library", "Home › Paragraph › Multilevel List", preset).params( + r#"{"preset": "headings" (1 / 1.1 / 1.1.1) | "article" (Article I. / Section 1.01 / (a)) | "outlineHeadings" (I. / A. / 1.) | "chapter" (Chapter 1, lower levels unnumbered)} — a new list whose nine levels are linked to Heading 1–9: every paragraph in those styles is numbered, selected paragraphs already in a Heading style keep it, and any other selected paragraph becomes Heading 1 (the first level)"#, + ), ] } @@ -226,6 +229,96 @@ fn define(s: &mut Session, v: &Value) -> CmdResult { Ok(r) } +/// The heading-linked lists of the Multilevel List gallery's library, by preset id: level `i` +/// is linked to Heading `i + 1`. +fn preset_levels(preset: &str) -> Option> { + use NumFormat as F; + let levels = (0..LEVELS) + .map(|i| { + let n = i + 1; + // Numbers start at the margin; each level's text a little further in. + let at = |indent: f32| Level { indent, hanging: indent, style: Some(format!("Heading{n}")), ..Level::default() }; + let (format, text, indent) = match preset { + "headings" => (F::Decimal, (1..=n).map(|k| format!("%{k}")).collect::>().join("."), 21.6 + 7.2 * i as f32), + "article" => match i { + 0 => (F::UpperRoman, "Article %1.".to_string(), 0.0), + 1 => (F::DecimalZero, "Section %1.%2".to_string(), 0.0), + _ => { + let f = [F::LowerLetter, F::LowerRoman, F::Decimal, F::LowerLetter, F::LowerRoman, F::LowerLetter, F::LowerRoman]; + let t = if i < 4 { + format!("(%{n})") + } else if i < 7 { + format!("%{n})") + } else { + format!("%{n}.") + }; + (f.get(i - 2).copied().unwrap_or(F::Decimal), t, 36.0 + 7.2 * (i - 2) as f32) + } + }, + "outlineHeadings" => { + let f = [ + F::UpperRoman, + F::UpperLetter, + F::Decimal, + F::LowerLetter, + F::Decimal, + F::LowerLetter, + F::LowerRoman, + F::LowerLetter, + F::LowerRoman, + ]; + let t = match i { + 0..=2 => format!("%{n}."), + 3 => format!("%{n})"), + _ => format!("(%{n})"), + }; + (f.get(i).copied().unwrap_or(F::Decimal), t, 36.0 * n as f32) + } + "chapter" if i == 0 => (F::Decimal, "Chapter %1".to_string(), 0.0), + "chapter" => (F::None, String::new(), 0.0), + _ => return None, + }; + // "Section 1.01" shows the article number in digits (legal numbering). + Some(Level { format, text, legal: preset == "article" && i == 1, ..at(indent) }) + }) + .collect::>>()?; + Some(levels) +} + +/// A Multilevel List gallery preset: a new list linked to Heading 1–9. The heading styles carry +/// the number, so every heading (and every heading added later) is numbered. Paragraphs in a +/// Heading style keep their level; any other paragraph under the caret or selection (Normal, +/// Quote, Title…) becomes Heading 1, as in Word. +fn preset(s: &mut Session, v: &Value) -> CmdResult { + let id = p::req_str(v, "preset")?; + let levels = preset_levels(id).ok_or_else(|| CmdError::Params("`preset` must be headings, article, outlineHeadings or chapter".into()))?; + let num = s.doc.numbering.try_add_levels(None, levels)?; + let mut linked = Vec::new(); + for n in 1..=LEVELS as u8 { + let id = super::para::ensure_heading_style(s, n); + if let Some(st) = s.doc.styles.get_mut(&id) { + st.para.numbering = Some(NumRef { num, level: n - 1 }); + linked.push(id); + } + } + touch_styles(s, &linked)?; + super::para::fmt(s, &|pp| { + // A heading keeps its style and level; any other paragraph (Normal, Quote, Title…) + // becomes the first level's heading, Heading 1. The number comes from the style. + if !pp.style.as_ref().is_some_and(|st| linked.contains(st)) { + pp.style = linked.first().cloned(); + } + pp.numbering = None; + pp.indent_left = None; + pp.indent_first = None; + pp.indent_left_chars = None; + pp.indent_first_chars = None; + })?; + let mut r = sel_result(s)?; + r["num"] = json!(num); + Ok(r) +} + /// Lay out again every paragraph in one of `nums`. fn touch_lists(s: &mut Session, nums: &[u32]) -> Result<(), CmdError> { touch_where(s, &|p| p.props.numbering.is_some_and(|n| nums.contains(&n.num))) @@ -309,4 +402,138 @@ mod tests { // Programmatic calls without levels are an error, not a dialog. assert!(s.run("list.define", &json!({})).is_err()); } + + /// Each body paragraph's number as laid out (`None`: no number), and its style. + fn numbered(s: &mut Session) -> Vec<(Option, String)> { + let l = s.layout(); + let labels: Vec> = l + .pages + .iter() + .flat_map(|p| &p.items) + .filter_map(|i| if let Placed::Lines { para, l0: 0, .. } = i { Some(para.label.as_ref().map(|lb| lb.text.clone())) } else { None }) + .collect(); + let styles = s.doc.body.iter().filter_map(|b| b.as_para()).map(|p| p.props.style.clone().unwrap_or_else(|| "Normal".into())); + labels.into_iter().zip(styles).collect() + } + + fn n(label: Option<&str>, style: &str) -> (Option, String) { + (label.map(str::to_string), style.to_string()) + } + + /// Paragraphs "One" (Heading 1), "Two" (Heading 2), "Three" (`third`), the caret in "One". + fn headings(third: &str) -> Session { + let mut s = Session::new(Document::new()); + for (i, (t, st)) in [("One", "Heading1"), ("Two", "Heading2"), ("Three", third)].iter().enumerate() { + if i > 0 { + s.run("text.newParagraph", &json!({})).unwrap(); + } + s.run("text.insert", &json!({"text": t})).unwrap(); + s.run("para.style", &json!({"style": st})).unwrap(); + } + s.sel = crate::Selection::caret(wordcraft_doc::Pos::body(0, 0)); + s + } + + /// #1793: the Multilevel List gallery's heading presets number headings through their styles + /// (Word: One=1, Two=1.1, Three=2), keep the styles, and survive a save to .docx and back. + #[test] + fn a_heading_preset_numbers_headings_and_keeps_their_styles() { + let mut s = headings("Heading1"); + s.run("list.multilevelPreset", &json!({"preset": "headings"})).unwrap(); + let want = [n(Some("1"), "Heading1"), n(Some("1.1"), "Heading2"), n(Some("2"), "Heading1")]; + assert_eq!(numbered(&mut s), want); + // The number comes from the style, so a heading added later is numbered too. + let h2 = s.doc.styles.get("Heading2").and_then(|st| st.para.numbering).unwrap(); + assert_eq!(h2.level, 1); + let back = wordcraft_docx::read(&wordcraft_docx::write(&s.doc).unwrap()).unwrap(); + assert_eq!(back.styles.get("Heading2").and_then(|st| st.para.numbering).map(|x| x.level), Some(1), "numPr on the style"); + let mut b = Session::new(back); + assert_eq!(numbered(&mut b), want); + } + + /// #1793: the other heading presets of Word's List Library, level by level. + #[test] + fn every_heading_preset_numbers_three_heading_levels() { + for (preset, want) in [ + ("headings", [Some("1"), Some("1.1"), Some("1.1.1")]), + ("article", [Some("Article I."), Some("Section 1.01"), Some("(a)")]), + ("outlineHeadings", [Some("I."), Some("A."), Some("1.")]), + ("chapter", [Some("Chapter 1"), None, None]), + ] { + let mut s = headings("Heading3"); + s.run("list.multilevelPreset", &json!({"preset": preset})).unwrap(); + let got: Vec> = numbered(&mut s).into_iter().map(|x| x.0).collect(); + assert_eq!(got, want.map(|x| x.map(str::to_string)), "{preset}"); + } + let mut s = headings("Heading3"); + assert!(s.run("list.multilevelPreset", &json!({"preset": "nope"})).is_err()); + assert!(s.run("list.multilevelPreset", &json!({})).is_err()); + } + + /// Paragraphs (text, style id), the selection from paragraph `from` to the end of `to`. + fn doc(paras: &[(&str, &str)], from: usize, to: usize) -> Session { + let mut s = Session::new(Document::new()); + for (i, (t, st)) in paras.iter().enumerate() { + if i > 0 { + s.run("text.newParagraph", &json!({})).unwrap(); + } + s.run("text.insert", &json!({"text": t})).unwrap(); + s.run("para.style", &json!({"style": st})).unwrap(); + } + let end = paras.get(to).map_or(0, |p| p.0.len()); + s.sel = crate::Selection { anchor: wordcraft_doc::Pos::body(from, 0), focus: wordcraft_doc::Pos::body(to, end) }; + s + } + + /// #1793 (Word for Mac 16.113.4, 2026-10-16): with a caret or a selection, a heading preset + /// leaves paragraphs already in a Heading style at their own style and level; every other + /// selected paragraph (Normal, Quote, Title…) becomes Heading 1. + #[test] + fn a_heading_preset_keeps_headings_and_makes_other_paragraphs_heading_1() { + let (h1, h2, h3) = ("Heading1", "Heading2", "Heading3"); + let cases: Vec<(Vec<(&str, &str)>, usize, usize, Vec<(Option<&str>, &str)>)> = vec![ + // All selected. + (vec![("One", h1), ("Two", h2), ("Three", h3)], 0, 2, vec![(Some("1"), h1), (Some("1.1"), h2), (Some("1.1.1"), h3)]), + // No Heading 1 above. + (vec![("Two", h2), ("Three", h3)], 0, 1, vec![(Some("1.1"), h2), (Some("1.1.1"), h3)]), + (vec![("Body", "Normal"), ("Three", h3)], 0, 1, vec![(Some("1"), h1), (Some("1.1.1"), h3)]), + (vec![("One", h1), ("Body", "Normal"), ("Two", h2)], 0, 2, vec![(Some("1"), h1), (Some("2"), h1), (Some("2.1"), h2)]), + (vec![("One", h1), ("Two", h2), ("Body", "Normal")], 0, 2, vec![(Some("1"), h1), (Some("1.1"), h2), (Some("2"), h1)]), + // The caret in Two. + (vec![("One", h1), ("Two", h2), ("Three", h3)], 1, 1, vec![(Some("1"), h1), (Some("1.1"), h2), (Some("1.1.1"), h3)]), + (vec![("Quote", "Quote"), ("Two", h2)], 0, 1, vec![(Some("1"), h1), (Some("1.1"), h2)]), + (vec![("Title", "Title")], 0, 0, vec![(Some("1"), h1)]), + // Body text after the selection stays unnumbered. + (vec![("One", h1), ("After", "Normal")], 0, 0, vec![(Some("1"), h1), (None, "Normal")]), + ]; + for (paras, from, to, want) in cases { + let mut s = doc(¶s, from, to); + s.run("list.multilevelPreset", &json!({"preset": "headings"})).unwrap(); + let want: Vec<_> = want.into_iter().map(|(l, st)| n(l, st)).collect(); + assert_eq!(numbered(&mut s), want, "{paras:?} {from}..={to}"); + } + } + + /// #1793 (Word for Mac): the caret alone in a Normal paragraph makes it Heading 1 numbered 1. + #[test] + fn a_heading_preset_on_a_lone_normal_paragraph_makes_it_heading_1() { + let mut s = doc(&[("Body", "Normal")], 0, 0); + s.sel = crate::Selection::caret(wordcraft_doc::Pos::body(0, 2)); + s.run("list.multilevelPreset", &json!({"preset": "headings"})).unwrap(); + assert_eq!(numbered(&mut s), [n(Some("1"), "Heading1")]); + } + + /// #1793: one Undo takes the preset back: paragraph styles and the heading styles' numbers. + #[test] + fn undo_takes_a_heading_preset_back_in_one_step() { + let mut s = doc(&[("Body", "Normal"), ("Two", "Heading2")], 0, 1); + let before = numbered(&mut s); + s.run("list.multilevelPreset", &json!({"preset": "headings"})).unwrap(); + assert_ne!(numbered(&mut s), before); + s.run("edit.undo", &json!({})).unwrap(); + assert_eq!(numbered(&mut s), before); + for n in 1..=9 { + assert_eq!(s.doc.styles.get(&format!("Heading{n}")).and_then(|st| st.para.numbering), None, "Heading{n}"); + } + } } diff --git a/crates/engine/src/cmd/para.rs b/crates/engine/src/cmd/para.rs index b20edbef3..2de476925 100644 --- a/crates/engine/src/cmd/para.rs +++ b/crates/engine/src/cmd/para.rs @@ -465,10 +465,13 @@ fn apply_style(s: &mut Session, v: &Value) -> CmdResult { return super::format::apply(s, &|c| c.style = Some(id.clone())); } let (a, b) = s.sel.ordered(); + // A style linked to a list level (a heading in a numbered-headings list) brings its number. + let numbered = s.doc.styles.resolve_para(&ParaProps { style: Some(id.clone()), ..Default::default() }).numbering.is_some(); fmt(s, &|p| { p.style = Some(id.clone()); - // Applying a heading or Normal removes direct list numbering (except list styles). - if (id.starts_with("Heading") || id == "Title" || id == "Normal") && p.numbering.is_some_and(|n| n.num != 0) { + // Applying a heading or Normal removes direct list numbering (except list styles); a + // numbered style's own number replaces any direct numbering, even numbering switched off. + if numbered || ((id.starts_with("Heading") || id == "Title" || id == "Normal") && p.numbering.is_some_and(|n| n.num != 0)) { p.numbering = None; } })?; @@ -755,7 +758,7 @@ pub(crate) fn set_heading(s: &mut Session, story: wordcraft_doc::StoryRef, path: /// Make sure the built-in `Heading{n}` style (and its linked character style) is in the /// document, for files that didn't bring it. -fn ensure_heading_style(s: &mut Session, n: u8) -> String { +pub(crate) fn ensure_heading_style(s: &mut Session, n: u8) -> String { let id = format!("Heading{n}"); if s.doc.styles.get(&id).is_none() { let builtin = wordcraft_doc::styles::StyleSheet::builtin(); diff --git a/crates/engine/src/cmd/text.rs b/crates/engine/src/cmd/text.rs index 39dbd168f..10bc18d48 100644 --- a/crates/engine/src/cmd/text.rs +++ b/crates/engine/src/cmd/text.rs @@ -171,7 +171,10 @@ fn list_autoformat(s: &mut Session) -> Result { let len = para.len(); para.delete(0, len)?; para.props.numbering = Some(NumRef { num, level: 0 }); - para.props.style = Some("ListParagraph".into()); + // A heading stays a heading (Word: "1. " in a Heading 1 gives a numbered Heading 1). + if para.props.style.as_deref().is_none_or(|st| st == "Normal") { + para.props.style = Some("ListParagraph".into()); + } s.sel = Selection::caret(Pos { off: 0, ..f }); Ok(true) } diff --git a/crates/engine/src/tests_typing.rs b/crates/engine/src/tests_typing.rs index 99da04e28..a30da9a1b 100644 --- a/crates/engine/src/tests_typing.rs +++ b/crates/engine/src/tests_typing.rs @@ -1486,3 +1486,92 @@ fn an_overlong_word_is_not_added_as_an_exception() { run(&mut s, "edit.undo", json!({})); assert!(first_letter_list(&s).is_empty()); } + +/// Each body paragraph's laid-out number (`None`: none) and its style. +fn numbers(s: &mut Session) -> Vec<(Option, String)> { + let l = s.layout(); + let labels: Vec> = l + .pages + .iter() + .flat_map(|p| &p.items) + .filter_map(|i| { + if let wordcraft_layout::Placed::Lines { para, l0: 0, .. } = i { Some(para.label.as_ref().map(|lb| lb.text.clone())) } else { None } + }) + .collect(); + let styles = s.doc.body.iter().filter_map(|b| b.as_para()).map(|p| p.props.style.clone().unwrap_or_else(|| "Normal".into())); + labels.into_iter().zip(styles).collect() +} + +fn num(label: Option<&str>, style: &str) -> (Option, String) { + (label.map(str::to_string), style.to_string()) +} + +/// #1793 (Word for Mac, 2026-10-15): after the "1 / 1.1 / 1.1.1" heading list, a new Heading 2 +/// typed after "Three" is numbered 2.1 on its own; the body text after it stays unnumbered. +#[test] +fn a_new_heading_after_a_heading_linked_list_is_numbered() { + let mut s = s(); + typ(&mut s, "One"); + run(&mut s, "para.heading1", json!({})); + typ(&mut s, "\nTwo"); + run(&mut s, "para.heading2", json!({})); + typ(&mut s, "\nThree"); + run(&mut s, "para.heading1", json!({})); + // The caret in "Three" (a Heading 1, the list's first level). + run(&mut s, "list.multilevelPreset", json!({"preset": "headings"})); + run(&mut s, "caret.docEnd", json!({})); + typ(&mut s, "\n"); + run(&mut s, "para.heading2", json!({})); + typ(&mut s, "Four\nBody"); + assert_eq!( + numbers(&mut s), + [num(Some("1"), "Heading1"), num(Some("1.1"), "Heading2"), num(Some("2"), "Heading1"), num(Some("2.1"), "Heading2"), num(None, "Normal")] + ); +} + +/// #1793: plain Numbering on a Heading 1 numbers it "1." and keeps Heading 1 (Word: "1. One"). +#[test] +fn numbering_a_heading_keeps_the_heading_style() { + let mut s = s(); + typ(&mut s, "One"); + run(&mut s, "para.heading1", json!({})); + run(&mut s, "para.numbering", json!({})); + assert_eq!(numbers(&mut s), [num(Some("1."), "Heading1")]); +} + +/// #1793: applying a heading style that carries list numbering numbers the paragraph, even one +/// whose numbering was switched off before. +#[test] +fn applying_a_numbered_heading_style_numbers_the_paragraph() { + let mut s = s(); + typ(&mut s, "One"); + run(&mut s, "para.heading1", json!({})); + run(&mut s, "list.multilevelPreset", json!({"preset": "headings"})); + typ(&mut s, "\nTwo"); + run(&mut s, "para.numbering", json!({})); + run(&mut s, "para.numbering", json!({"off": true})); + assert_eq!(numbers(&mut s)[1].0, None); + run(&mut s, "para.heading2", json!({})); + assert_eq!(numbers(&mut s), [num(Some("1"), "Heading1"), num(Some("1.1"), "Heading2")]); +} + +/// #1793: "1. " typed at the start of a heading numbers it and keeps the heading style; only +/// body text becomes List Paragraph. +#[test] +fn typing_1_dot_in_a_heading_keeps_the_heading_style() { + let mut s = s(); + run(&mut s, "para.heading1", json!({})); + typ(&mut s, "1. One\n"); + run(&mut s, "para.normal", json!({})); + typ(&mut s, "1. Body"); + assert_eq!(numbers(&mut s), [num(Some("1."), "Heading1"), num(Some("1."), "ListParagraph")]); + for (style, keys) in [("Heading2", "a) Part"), ("Subtitle", "1. Item")] { + let mut s = self::s(); + run(&mut s, "para.style", json!({"style": style})); + typ(&mut s, keys); + let got = numbers(&mut s); + assert_eq!(got.len(), 1); + assert!(got[0].0.is_some(), "{style}: a list"); + assert_eq!(got[0].1, style); + } +} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index f68232f38..9047f9f15 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -515,6 +515,10 @@ More templates → Още шаблони → Move or Resize Преместване или преоразмеряване Multilevel List Многостепенен списък + Multilevel List Library Библиотека с многостепенни списъци + Article I. Член I. + Chapter 1 Глава 1 + Section 1.01 Раздел 1.01 Multiple Pages Няколко страници Name (A–Z) Име (А–Я) Name: Име: diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 740f5a41a..21ae7e517 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -711,6 +711,10 @@ Move Past Citation Přejít za citaci Move Past\nCitation Přejít za\ncitaci Multilevel List Víceúrovňový seznam + Multilevel List Library Knihovna víceúrovňových seznamů + Article I. Článek I. + Chapter 1 Kapitola 1 + Section 1.01 Oddíl 1.01 Multiple Pages Více stránek Name (A–Z) Jméno (A–Z) Name of the file this document is saved as Název souboru dokumentu diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 87baf70e8..f37087f73 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -511,6 +511,10 @@ Modify… Ändern… Move or Resize Verschieben oder Größe ändern Multilevel List Mehrstufige Liste + Multilevel List Library Bibliothek mehrstufiger Listen + Article I. Artikel I. + Chapter 1 Kapitel 1 + Section 1.01 Abschnitt 1.01 Multiple Pages Mehrere Seiten Name (A–Z) Name (A–Z) Name: Name: diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 7156e4abd..329640b13 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -583,6 +583,10 @@ More templates → Más plantillas → Move or Resize Mover o cambiar tamaño Multilevel List Lista multinivel + Multilevel List Library Biblioteca de listas multinivel + Article I. Artículo I. + Chapter 1 Capítulo 1 + Section 1.01 Sección 1.01 Multiple Pages Varias páginas Name (A–Z) Nombre (A–Z) Name: Nombre: diff --git a/crates/ui-egui/src/i18n/et.tsv b/crates/ui-egui/src/i18n/et.tsv index 8c54632af..930099c0a 100644 --- a/crates/ui-egui/src/i18n/et.tsv +++ b/crates/ui-egui/src/i18n/et.tsv @@ -459,6 +459,10 @@ More templates → Veel malle → Move or Resize Teisalda või muuda suurust Multilevel List Mitmetasemeline loend + Multilevel List Library Mitmetasemeliste loendite teek + Article I. Artikkel I. + Chapter 1 Peatükk 1 + Section 1.01 Jaotis 1.01 Multiple Pages Mitu lehte Name (A–Z) Nimi (A–Z) Name: Nimi: diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 532c5902f..85bcb8345 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -525,6 +525,10 @@ Modify… Modifier… Move or Resize Déplacer ou redimensionner Multilevel List Liste à plusieurs niveaux + Multilevel List Library Bibliothèque de listes à plusieurs niveaux + Article I. Article I. + Chapter 1 Chapitre 1 + Section 1.01 Section 1.01 Multiple Pages Plusieurs pages Name (A–Z) Nom (A–Z) Name: Nom : diff --git a/crates/ui-egui/src/i18n/hr.tsv b/crates/ui-egui/src/i18n/hr.tsv index fb73a4394..dbdfd641e 100644 --- a/crates/ui-egui/src/i18n/hr.tsv +++ b/crates/ui-egui/src/i18n/hr.tsv @@ -1365,6 +1365,10 @@ template Letter Pismo Moved to Premješteno u Moves Premještanja Multilevel List Višerazinski popis + Multilevel List Library Biblioteka višerazinskih popisa + Article I. Članak I. + Chapter 1 Poglavlje 1 + Section 1.01 Odjeljak 1.01 Multiple Pages Više stranica Name Naziv Name (A–Z) Naziv (A–Ž) diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 4a4b85164..7567e1b6c 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -440,6 +440,10 @@ Modify Style Stílus módosítása Modify… Módosítás… Multilevel List Többszintű lista + Multilevel List Library Többszintű listák tára + Article I. I. cikk + Chapter 1 1. fejezet + Section 1.01 1.01. szakasz Multiple Pages Több oldal Name (A–Z) Név (A–Z) Name: Név: diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 3ca09c0fc..01b0c3c73 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -511,6 +511,10 @@ More templates → Altri modelli → Move or Resize Sposta o ridimensiona Multilevel List Elenco a più livelli + Multilevel List Library Raccolta elenchi a più livelli + Article I. Articolo I. + Chapter 1 Capitolo 1 + Section 1.01 Sezione 1.01 Multiple Pages Più pagine Name (A–Z) Nome (A–Z) Name: Nome: diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 5b649c0b5..1692407ca 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -568,6 +568,10 @@ Modify… 変更… Move or Resize 移動またはサイズ変更 Multilevel List アウトライン + Multilevel List Library アウトライン ライブラリ + Article I. 第 I 条 + Chapter 1 第 1 章 + Section 1.01 第 1.01 節 Multiple Pages 複数ページ Name (A–Z) 名前(A–Z) Name: 名前: diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index ac3bca891..d8de36366 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -562,6 +562,10 @@ Modify… 수정… Move or Resize 이동 또는 크기 조정 Multilevel List 다단계 목록 + Multilevel List Library 다단계 목록 라이브러리 + Article I. 제 I 조 + Chapter 1 제 1 장 + Section 1.01 제 1.01 절 Multiple Pages 여러 페이지 Name (A–Z) 이름순(A–Z) Name: 이름: diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 7fc07fbc0..d416d6994 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -431,6 +431,10 @@ More templates → Więcej szablonów → Move or Resize Przenieś lub zmień rozmiar Multilevel List Lista wielopoziomowa + Multilevel List Library Biblioteka list wielopoziomowych + Article I. Artykuł I. + Chapter 1 Rozdział 1 + Section 1.01 Sekcja 1.01 Multiple Pages Wiele stron Name (A–Z) Nazwa (A–Z) Name: Nazwa: diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 4ab9294fb..d40fa3bb0 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -569,6 +569,10 @@ Modify… Modificar… Move or Resize Mover ou redimensionar Multilevel List Lista de vários níveis + Multilevel List Library Biblioteca de listas de vários níveis + Article I. Artigo I. + Chapter 1 Capítulo 1 + Section 1.01 Seção 1.01 Multiple Pages Várias páginas Name (A–Z) Nome (A–Z) Name: Nome: diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index b549ff6fb..50500425f 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -442,6 +442,10 @@ Modify Style Изменить стиль Modify… Изменить… Multilevel List Многоуровневый список + Multilevel List Library Библиотека многоуровневых списков + Article I. Статья I. + Chapter 1 Глава 1 + Section 1.01 Раздел 1.01 Multiple Pages Несколько страниц Name (A–Z) Имя (А–Я) Name: Имя: diff --git a/crates/ui-egui/src/i18n/sr-latn.tsv b/crates/ui-egui/src/i18n/sr-latn.tsv index 64654587b..e80d2e36d 100644 --- a/crates/ui-egui/src/i18n/sr-latn.tsv +++ b/crates/ui-egui/src/i18n/sr-latn.tsv @@ -616,6 +616,10 @@ Modify… Izmeni… Move or Resize Pomeri ili promeni veličinu Multilevel List Višenivoska lista + Multilevel List Library Biblioteka višenivoskih lista + Article I. Član I. + Chapter 1 Poglavlje 1 + Section 1.01 Odeljak 1.01 Multiple Pages Više strana Name (A–Z) Naziv (A–Š) Name: Naziv: diff --git a/crates/ui-egui/src/i18n/sr.tsv b/crates/ui-egui/src/i18n/sr.tsv index e2aec0016..2111d1ebc 100644 --- a/crates/ui-egui/src/i18n/sr.tsv +++ b/crates/ui-egui/src/i18n/sr.tsv @@ -620,6 +620,10 @@ Modify… Измени… Move or Resize Помери или промени величину Multilevel List Вишенивоска листа + Multilevel List Library Библиотека вишенивоских листа + Article I. Члан I. + Chapter 1 Поглавље 1 + Section 1.01 Одељак 1.01 Multiple Pages Више страна Name (A–Z) Назив (А–Ш) Name: Назив: diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index adedea7f6..0ed9b6695 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -574,6 +574,10 @@ More templates → Fler mallar → Move or Resize Flytta eller ändra storlek Multilevel List Lista med flera nivåer + Multilevel List Library Bibliotek med listor med flera nivåer + Article I. Artikel I. + Chapter 1 Kapitel 1 + Section 1.01 Avsnitt 1.01 Multiple Pages Flera sidor Name (A–Z) Namn (A–Ö) Name: Namn: diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 85de2ce6a..1ae4d1fa4 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -440,6 +440,10 @@ Modify Style Stili Değiştir Modify… Değiştir… Multilevel List Çok Düzeyli Liste + Multilevel List Library Çok Düzeyli Liste Kitaplığı + Article I. Madde I. + Chapter 1 Bölüm 1 + Section 1.01 Kısım 1.01 Multiple Pages Birden Çok Sayfa Name (A–Z) Ad (A–Z) Name: Ad: diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 33676227d..a29fd94be 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -583,6 +583,10 @@ Modify… Змінити… Move or Resize Перемістити або змінити розмір Multilevel List Багаторівневий список + Multilevel List Library Бібліотека багаторівневих списків + Article I. Стаття I. + Chapter 1 Глава 1 + Section 1.01 Розділ 1.01 Multiple Pages Кілька сторінок Name (A–Z) Назва (А–Я) Name: Назва: diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 9f024b434..efdfe9229 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -507,6 +507,10 @@ More templates → Xem thêm mẫu → Move or Resize Di chuyển hoặc đổi kích thước Multilevel List Danh sách đa cấp + Multilevel List Library Thư viện danh sách đa cấp + Article I. Điều I. + Chapter 1 Chương 1 + Section 1.01 Mục 1.01 Multiple Pages Nhiều trang Name (A–Z) Tên (A–Z) Name: Tên: diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index adf421d6b..217e48fbd 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -580,6 +580,10 @@ Modify… 修改… Move or Resize 移动或调整大小 Multilevel List 多级列表 + Multilevel List Library 多级列表库 + Article I. 第 I 条 + Chapter 1 第 1 章 + Section 1.01 第 1.01 节 Multiple Pages 多页 Name (A–Z) 名称(A–Z) Name: 名称: diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 4489c5119..367e29907 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -569,6 +569,10 @@ Modify… 修改… Move or Resize 移動或調整大小 Multilevel List 多層次清單 + Multilevel List Library 多層次清單庫 + Article I. 第 I 條 + Chapter 1 第 1 章 + Section 1.01 第 1.01 節 Multiple Pages 多頁 Name (A–Z) 名稱(A–Z) Name: 名稱: diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 088378a5d..cd7083309 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -1081,6 +1081,23 @@ fn home(app: &mut WordApp, ui: &mut Ui) { mi(ui, app, "1. 1.1. 1.1.1.", "para.multilevel", json!({"kind": "legal"})); mi(ui, app, "I. A. 1. a.", "para.multilevel", json!({"kind": "outline"})); ui.separator(); + // Lists linked to the heading styles: the headings carry the numbers. Each item + // reads as its first three levels, number then (translated) style name. + for (numbers, k) in [ + (["1", "1.1", "1.1.1"], "headings"), + (["Article I.", "Section 1.01", "(a)"], "article"), + (["I.", "A.", "1."], "outlineHeadings"), + (["Chapter 1", "", ""], "chapter"), + ] { + let text = numbers + .iter() + .zip(["Heading 1", "Heading 2", "Heading 3"]) + .map(|(n, h)| if n.is_empty() { tl!(h).to_string() } else { format!("{} {}", tl!(n), tl!(h)) }) + .collect::>() + .join(" / "); + mi_text(ui, app, &text, "list.multilevelPreset", json!({"preset": k})); + } + ui.separator(); mi(ui, app, "Define New Multilevel List…", "list.define", json!({})); ui.separator(); for lv in 0..5u64 { diff --git a/docs/gaps.md b/docs/gaps.md index 17b17cd7d..48a9e88d1 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-18 · **Change:** trivial (legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: #8 narrowed: Indonesian proofing from the installed Hunspell dictionary, #498; previously trivial: glossary/building blocks closed; previously trivial: Split Cells and Convert Text to Table follow the Word check, #2592; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: #29: a copied equation also goes in the MathML clipboard formats, #1297; previously trivial: VML freeforms read and kept on save; previously trivial: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: floating-table overlap ignores empty line-break lines; previously trivial: copies keep their formatting on the clipboard as HTML, #1523; previously 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 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** trivial (numbered headings, #1793; previously trivial: legacy outline, shadow, emboss and imprint text effects drawn, #2534; previously trivial: #8 narrowed: Indonesian proofing from the installed Hunspell dictionary, #498; previously trivial: glossary/building blocks closed; previously trivial: Split Cells and Convert Text to Table follow the Word check, #2592; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717); previously trivial: #29: a copied equation also goes in the MathML clipboard formats, #1297; previously trivial: VML freeforms read and kept on save; previously trivial: consistent preferred cell widths; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: floating-table overlap ignores empty line-break lines; previously trivial: copies keep their formatting on the clipboard as HTML, #1523; previously 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. @@ -99,6 +99,7 @@ The Language dialog reads effective character formatting, including document def | 2026-10-16 | trivial | #7: Split Cells and Convert Text to Table dialogs (#2592), 58 modal dialogs (matches ui-parity.md) | | 2026-10-16 | trivial | #8 narrowed: Indonesian proofing (id-ID spelling from the installed Hunspell dictionary, generic checks, #498) | | 2026-10-16 | trivial | Legacy outline, shadow, emboss and imprint text effects drawn (#2534) | +| 2026-10-15 | trivial | Numbered headings (#1793): heading-linked Multilevel List presets; numbering keeps heading styles. Not a ranked gap; style gallery previews without numbers remain | | 2026-10-14 | trivial | Row table border exceptions fixed; other row table exceptions remain unsupported | | 2026-10-12 | minor | Czech proofing review: short-word splits rank after deletion, NOSUGGEST excludes suggestions while accepting typed forms, affix comments apply only to headers, ratios keep spaces and repeated-word messages use Czech quotes; synthetic engine end-to-end and latency checks | | 2026-10-12 | minor | Czech spelling from the installed Hunspell dictionary, runtime-table suggestions and generic checks; Czech interface writes cs-CZ documents; no Czech hyphenation patterns bundled; estimates unchanged | diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 0f22c6d71..8a9375bcc 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -2,20 +2,20 @@ Generated by `cargo xtask parity` (`wordcraft-cli parity --markdown`): the word-processor feature catalog (`crates/engine/src/catalog.rs`) compared with the live command registry. -**487 of 502 catalog features have commands (97.0%).** +**490 of 505 catalog features have commands (97.0%).** | Tab | Live | Total | |---|---|---| | File | 22 | 22 | | Quick Access | 5 | 5 | | Title Bar | 1 | 1 | -| Home | 102 | 103 | +| Home | 103 | 104 | | Insert | 47 | 53 | | Draw | 10 | 11 | | Design | 10 | 10 | | Layout | 22 | 22 | | References | 25 | 25 | -| Mailings | 17 | 17 | +| Mailings | 19 | 19 | | Review | 29 | 31 | | View | 25 | 29 | | Developer | 11 | 11 | diff --git a/docs/typing-parity.md b/docs/typing-parity.md index 4f53c5c37..593d35d80 100644 --- a/docs/typing-parity.md +++ b/docs/typing-parity.md @@ -1,6 +1,6 @@ # Typing parity with Word -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-18 · **Change:** trivial (reversing an automatic capital adds an exception, #1857; previously trivial: smart quotes open after guillemets and low quotes and nest; previously trivial: Enter on an empty list item numbered by its paragraph style moves it up a level, #2143; previously trivial: Backspace after a section break and Delete before it remove the break, also on the last paragraph, #2127; previously trivial: a table inserted in right-to-left text is right to left, so Tab moves left, #2634; previously trivial: Shift+Delete cuts the selection on every platform, #1669; previously trivial: Select All then Delete removes a table the document starts with, #1198; previously trivial: Alt+numpad character codes, #961; previously trivial: Down and Up from a line start beside a table go into it, #1007; previously trivial: sample-text formulas on Enter, #1067; previously trivial: Track Changes: Accept All after a tracked delete from a paragraph end keeps the next paragraph; Reject All after typing into an empty paragraph keeps it (#864); previously minor: Correct TWo INitial CApitals, cAPS LOCK and table-cell capitals, #544; previously trivial: complex-script caret and Backspace, #604; Move Paragraph Up/Down, Promote/Demote, Go Back and Alt+X keys, #343) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-18 · **Change:** trivial (a heading-list preset keeps Heading styles and makes other paragraphs Heading 1, as in Word, #1793; previously trivial: numbered headings: a heading-linked Multilevel List numbers new headings by their style; numbering keeps a heading a heading, #1793; previously trivial: reversing an automatic capital adds an exception, #1857; previously trivial: smart quotes open after guillemets and low quotes and nest; previously trivial: Enter on an empty list item numbered by its paragraph style moves it up a level, #2143; previously trivial: Backspace after a section break and Delete before it remove the break, also on the last paragraph, #2127; previously trivial: a table inserted in right-to-left text is right to left, so Tab moves left, #2634; previously trivial: Shift+Delete cuts the selection on every platform, #1669; previously trivial: Select All then Delete removes a table the document starts with, #1198; previously trivial: Alt+numpad character codes, #961; previously trivial: Down and Up from a line start beside a table go into it, #1007; previously trivial: sample-text formulas on Enter, #1067; previously trivial: Track Changes: Accept All after a tracked delete from a paragraph end keeps the next paragraph; Reject All after typing into an empty paragraph keeps it (#864); previously minor: Correct TWo INitial CApitals, cAPS LOCK and table-cell capitals, #544; previously trivial: complex-script caret and Backspace, #604; Move Paragraph Up/Down, Promote/Demote, Go Back and Alt+X keys, #343) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 What Word does when you type, observed black-box (same keystrokes typed into Word and WordCraft, results read back paragraph by paragraph) and pinned by tests in @@ -40,6 +40,12 @@ formatting use the existing editing primitives. Tests: `sample_text_formulas_exp | `* ` `- ` `1. ` `1) ` `a. ` `a) ` `A. ` `i. ` `I. ` + text | bullet, dash bullet, 1. 1) a. a) A. i. I. lists | `list_autoformat_triggers` | | `> ` `-> ` `=> ` + text | bulleted list with an arrow bullet (➢ ➔ ⇨; Word takes them from a symbol font, we use the Unicode arrows) ¹ | `arrow_bullets` | | `1. ` after other text | a new list restarting at 1 (never continues an earlier list) | `typing_1_dot_starts_numbering_again` | +| Heading 1 `One`, Heading 2 `Two`, Heading 1 `Three`, the caret in `Three`, Multilevel List › the "1 / 1.1 / 1.1.1" heading list, then at the end Enter, Heading 2, `Four` Enter `Body` | One = 1, Two = 1.1, Three = 2, styles kept; the new Heading 2 is numbered 2.1 by itself (the heading styles carry the list); `Body` (Normal) stays unnumbered | `a_new_heading_after_a_heading_linked_list_is_numbered` | +| Numbering on a Heading 1 | "1. One", still Heading 1 | `numbering_a_heading_keeps_the_heading_style` | +| The "1 / 1.1 / 1.1.1" heading list with a caret or a selection | paragraphs already in a Heading style keep their style and level; every other paragraph style (Normal, Quote, Title…) becomes Heading 1. One H1 / Body / Two H2 → 1, Body H1 "2", Two H2 "2.1"; Two H2 + Three H3 with no Heading 1 above → 1.1, 1.1.1; caret in a Heading 2 → stays Heading 2 "1.1"; Normal alone → Heading 1 "1" (checked in Word for Mac, 2026-10-16) | `a_heading_preset_keeps_headings_and_makes_other_paragraphs_heading_1`, `a_heading_preset_on_a_lone_normal_paragraph_makes_it_heading_1` | +| Undo after a heading list | one step: paragraph styles and the heading styles' numbers come back | `undo_takes_a_heading_preset_back_in_one_step` | +| A paragraph whose numbering was switched off, then Heading 2 from a heading-linked list | numbered (1.1) by the style | `applying_a_numbered_heading_style_numbers_the_paragraph` | +| `1. ` typed in a Heading 1 (also `a) ` in a Heading 2, `1. ` in a Subtitle) | numbered, the style kept; only body text becomes List Paragraph (checked in Word for Mac, 2026-10-15) | `typing_1_dot_in_a_heading_keeps_the_heading_style` | | ⌘Z right after an AutoFormat | undoes only the AutoFormat (`* ` comes back as text) | `undo_right_after_autoformat_undoes_only_the_autoformat` | ## Undo @@ -230,6 +236,9 @@ Word. | 2026-10-15 | trivial | Enter on an empty nested list item whose numbering comes from its paragraph style (List Number 2) moves it up a level instead of adding another item (#2143) | | 2026-10-18 | trivial | Smart quotes open after `«` `‹` `„` `‚` and after the other kind of opening quote, so nested quotes curl correctly | | 2026-10-15 | minor | AutoCorrect: undoing the capital's own step (or backspacing it and retyping) adds an exception; "Automatically add words to list" switches (#1857) | +| 2026-10-16 | trivial | #1793: a heading-list preset keeps paragraphs already in a Heading style at their level and makes other paragraphs Heading 1 (rechecked in Word for Mac); undo test | +| 2026-10-16 | trivial | #1793: a heading-linked Multilevel List preset makes every selected paragraph its first level (Heading 1), checked in Word for Mac; `1. ` in a heading checked in Word; a single Heading 2 becoming Heading 1 is unchecked | +| 2026-10-15 | trivial | Numbered headings (#1793): a heading-linked Multilevel List numbers headings through their styles, so a new Heading 2 after the list is numbered by itself; Numbering and `1. ` keep a heading a heading; applying a numbered heading style numbers the paragraph | | 2026-10-14 | trivial | Shift+Delete cuts the selection, and does nothing without one, on macOS and Linux too, as in Word for Windows (#1669); Shift+Backspace already erased (#707) | | 2026-10-13 | trivial | Select All then Delete or Backspace removes a table the document starts with (#1198) | | 2026-10-12 | trivial | Editing keys: Down and Up from the start of a line beside a table go into it instead of skipping it (#1007) | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 192beeb86..290d77212 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-18 · **Change:** trivial (Navigation Find exposes Results, #2096; previously trivial: the tab strip never draws tabs over Comments / Editing / Share, #2026; previously trivial: Columns on selected text, #2024; previously trivial: Different First Page headers reachable from page 1, #2826; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: Read Mode keeps the keyboard, shows whole columns and keeps text clear of the page-turn bar, #1772); previously trivial: font list entries of tall-ascent fonts sit in their row, not cut off, #2570); previously trivial: Split Cells and Convert Text to Table dialogs follow Word's merged-cell limits and Initial column width, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout text fills the window width, #1876; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717)); previously trivial: comment balloons readable in the dark theme, #2045; previously trivial: Recently Used Fonts section at the top of the font list, #2068; previously trivial: web preferences kept between visits, #2132; previously trivial: dialog close control: a Windows-style box at the title bar's right end, the whole box clickable, red on hover, #1885); previously trivial (Modify Table Style formats every table style part, #714); previously trivial: a click on the ribbon chrome hands the keyboard to the page in the same frame, #1093 follow-up; previously trivial: Page Setup vertical alignment, #1320; previously trivial: the app opens on the Home tab every launch, as in Word; previously trivial: no document behind the Start screen, #1129; previously trivial: equation pictures share a few textures, #1002; 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-18 · **Change:** trivial (numbered headings, #1793; previously trivial: Navigation Find exposes Results, #2096; previously trivial: the tab strip never draws tabs over Comments / Editing / Share, #2026; previously trivial: Columns on selected text, #2024; previously trivial: Different First Page headers reachable from page 1, #2826; previously trivial: a copied equation also goes in the clipboard's MathML formats, #1297; previously trivial: Read Mode keeps the keyboard, shows whole columns and keeps text clear of the page-turn bar, #1772); previously trivial: font list entries of tall-ascent fonts sit in their row, not cut off, #2570); previously trivial: Split Cells and Convert Text to Table dialogs follow Word's merged-cell limits and Initial column width, #2592; previously trivial: a new source's Tag is made like Word's, three letters of the author and two digits of the year with a number added when taken, and an edit keeps the Tag, #2094; previously trivial: Web Layout text fills the window width, #1876; previously trivial: Language dialog keeps inherited proofing language and no-proof choices (#2717)); previously trivial: comment balloons readable in the dark theme, #2045; previously trivial: Recently Used Fonts section at the top of the font list, #2068; previously trivial: web preferences kept between visits, #2132; previously trivial: dialog close control: a Windows-style box at the title bar's right end, the whole box clickable, red on hover, #1885); previously trivial (Modify Table Style formats every table style part, #714); previously trivial: a click on the ribbon chrome hands the keyboard to the page in the same frame, #1093 follow-up; previously trivial: Page Setup vertical alignment, #1320; previously trivial: the app opens on the Home tab every launch, as in Word; previously trivial: no document behind the Start screen, #1129; previously trivial: equation pictures share a few textures, #1002; 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 @@ -80,6 +80,9 @@ Find selects the Results tab, including when it prefills selected text. Editing | 2026-10-15 | trivial | Layout › Columns works on the selected text, as in Word: section breaks around it, the rest of the document unchanged; More Columns… adds "Selected text" to Apply to and picks it when text is selected (#2024) | | 2026-10-15 | trivial | Navigation Find and query edits expose Results (#2096) | | 2026-10-15 | trivial | AutoCorrect Options › Exceptions: "Automatically add words to list" switches for First Letter and INitial CAps (#1857) | +| 2026-10-16 | trivial | Multilevel List heading presets: every selected paragraph takes the first level (Heading 1), as Word for Mac does; headings outside the selection keep their styles and levels (#1793) | +| 2026-10-16 | trivial | Multilevel List heading presets: selected paragraphs already in a Heading style keep their style and level, every other selected paragraph becomes Heading 1, as Word for Mac does (#1793) | +| 2026-10-15 | trivial | Multilevel List gallery: four lists linked to Heading 1–9 (1 / 1.1 / 1.1.1; Article I. / Section 1.01 / (a); I. / A. / 1.; Chapter 1), `list.multilevelPreset`; headings keep their styles and new headings are numbered (#1793). Style gallery previews don't show the numbers yet | | 2026-10-13 | trivial | New/Modify Table Style: No Border on a part that inherits its lines hides them; emptying the font box makes the part's font inherited again (#1288) | | 2026-10-14 | trivial | Tab stops edited on the ruler: corner kind selector, click to set, drag to move or off to clear, per-kind markers mirrored for RTL; each gesture one `para.tabs` call and one undo step; Rulers 85% → 90% | | 2026-10-12 | trivial | Tables on the page: one selected cell (Select Cell, or a click along its left edge) is a cell selection when its border is dragged: the border moves in its row only (#962) |