Numbered headings: heading-linked multilevel list presets keep the heading styles (#1793) (#2239)

* Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #1793: wording: other paragraph styles become Heading 1, not only body text

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Dmitrii Khlapov
2026-10-18 04:22:12 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Dmitrii Khlapov Brandon Thomas
parent 8cdd85f7e7
commit e7f5d32c35
33 changed files with 454 additions and 11 deletions
+3 -1
View File
@@ -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 |
+1
View File
@@ -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
+227
View File
@@ -23,6 +23,9 @@ pub fn specs() -> Vec<CommandSpec> {
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<Vec<Level>> {
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::<Vec<_>>().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::<Option<Vec<_>>>()?;
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>, String)> {
let l = s.layout();
let labels: Vec<Option<String>> = 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>, 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<Option<String>> = 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(&paras, 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}");
}
}
}
+6 -3
View File
@@ -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();
+4 -1
View File
@@ -171,7 +171,10 @@ fn list_autoformat(s: &mut Session) -> Result<bool, CmdError> {
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)
}
+89
View File
@@ -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>, String)> {
let l = s.layout();
let labels: Vec<Option<String>> = 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>, 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);
}
}
+4
View File
@@ -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: Име:
Can't render this file because it contains an unexpected character in line 7 and column 25.
+4
View File
@@ -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
Can't render this file because it contains an unexpected character in line 7 and column 25.
+4
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 6 and column 25.
+4
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 6 and column 25.
+4
View File
@@ -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:
1 # Estonian (et) interface catalog. Columns separated by a TAB:
459
460
461
462
463
464
465
466
467
468
+4
View File
@@ -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 :
Can't render this file because it contains an unexpected character in line 7 and column 25.
+4
View File
@@ -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–Ž)
Can't render this file because it contains an unexpected character in line 6 and column 25.
+4
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 6 and column 25.
+4
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 6 and column 25.
+4
View File
@@ -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: 名前:
1 # Japanese (ja) interface catalog. Same format and clean-room rules as zh-hans.tsv.
568
569
570
571
572
573
574
575
576
577
+4
View File
@@ -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: 이름:
1 # Korean (ko) interface catalog. Same format and clean-room rules as zh-hans.tsv.
562
563
564
565
566
567
568
569
570
571
+4
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 6 and column 25.
+4
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 6 and column 25.
+4
View File
@@ -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: Имя:
Can't render this file because it contains an unexpected character in line 7 and column 25.
+4
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 6 and column 25.
+4
View File
@@ -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: Назив:
Can't render this file because it contains an unexpected character in line 11 and column 25.
+4
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 7 and column 25.
+4
View File
@@ -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:
1 # Turkish (tr) interface catalog. Columns, separated by a TAB:
440
441
442
443
444
445
446
447
448
449
+4
View File
@@ -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: Назва:
Can't render this file because it contains an unexpected character in line 6 and column 31.
+4
View File
@@ -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:
Can't render this file because it contains an unexpected character in line 7 and column 25.
+4
View File
@@ -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: 名称:
Can't render this file because it contains an unexpected character in line 7 and column 25.
+4
View File
@@ -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: 名稱:
1 # Traditional Chinese (zh-hant, Taiwan vocabulary) interface catalog. Same format and clean-room
569
570
571
572
573
574
575
576
577
578
+17
View File
@@ -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::<Vec<_>>()
.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 {
+2 -1
View File
@@ -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 |
+3 -3
View File
@@ -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 |
+10 -1
View File
@@ -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) |
+4 -1
View File
@@ -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) |