mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-24 10:27:44 -06:00
* 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:
co-authored by
Claude Opus 5.5
Dmitrii Khlapov
Brandon Thomas
parent
8cdd85f7e7
commit
e7f5d32c35
+3
-1
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(¶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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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:
|
||||
|
||||
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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: 名前:
|
||||
|
||||
|
@@ -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: 이름:
|
||||
|
||||
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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:
|
||||
|
||||
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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: 名稱:
|
||||
|
||||
|
@@ -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
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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
@@ -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) |
|
||||
|
||||
Reference in New Issue
Block a user