From 2a1cdbb17fddc77ed3f695076b08c828fb6a2b9c Mon Sep 17 00:00:00 2001 From: Alik <86338858+alik654zx@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:37:37 +0500 Subject: [PATCH] =?UTF-8?q?Lists:=20Russian=20letter=20numbering=20draws?= =?UTF-8?q?=20=D0=B0)=20=D0=B1)=20=D0=B2)=20instead=20of=20digits=20(#1577?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit russianLower/russianUpper showed 1), 2), 3). They now follow Word's 29-letter sequence (no ё, й, ъ or ь; ы kept), then repeat the letter (аа, бб, ааа), as observed black-box in Word. Co-authored-by: Brandon Thomas --- ROADMAP.md | 4 +++- crates/doc/src/section.rs | 35 ++++++++++++++++++++++++++++++++--- crates/layout/src/tests.rs | 31 +++++++++++++++++++++++++++++++ docs/layout-parity.md | 5 +++-- 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 58ec4a1d8..eea2e98ad 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: justified vertical page alignment, #1320; previously trivial: progress log: file names that aren't valid Unicode, #1440; previously trivial: Behind Text pictures and shapes can be dragged across overlaid text, in the body and headers, without taking ordinary text clicks; previously trivial: progress log: crop handles on the canvas; previously trivial: progress log: Insert › Bookmark deletes and goes to bookmarks, #1236; previously trivial: progress log: an option to show only the selected text's comment, #1055; previously trivial: #778 mail-merge recipient filter/sort, .xls and legacy encodings; previously trivial: progress log: clicking in a comment's text moves its caret, #1013; previously trivial: progress log: a press on the ribbon no longer scrolls the document, #1019; previously trivial: title rename and account settings; previously trivial: progress log: Down and Up no longer skip a table, #1007; 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-14 · **Change:** trivial (progress log: justified vertical page alignment, #1320; previously trivial: progress log: Russian letter list labels; 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. @@ -161,6 +161,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | 2026-10-13 | `wordcraft --control` checks its port (#1426): a missing port, one outside 1–65535 or one that isn't a number prints an error naming it and exits with status 2, instead of starting without the control channel (and dropping `WORDCRAFT_CONTROL_PORT`); a flag right after it, such as `--sample` or `--version`, is read as a flag rather than as the port | | 2026-10-14 | Text wrap (#1376): in the narrow strip beside a square-wrapped picture, a word that doesn't fit moves on whole to the next row; it used to be split across the strip ("docu" / "mentazione") | | 2026-10-14 | DOCX files with legacy VML preset shapes show them (#1377): Word's right arrow (`_x0000_t13`) and the other presets appear with their fill, outline and size, drawn as WordCraft draws the same DrawingML preset (the nearest shape when it has no exact one), and saving keeps them as DrawingML shapes; before, they were dropped | +| 2026-10-14 | Lists numbered with Russian letters (`russianLower`, `russianUpper`) show а), б), в) … я), then аа), бб), as in Word, instead of 1), 2), 3); the 29-letter sequence (no ё, й, ъ or ь) was observed in Word | | 2026-10-12 | Text boxes from older Word files (VML) keep their internal margins, Resize shape to fit text and vertical alignment, so letterhead address blocks and form headers sit where Word puts them instead of 7 pt in and 4 pt down. Hidden drawings stay hidden (they were drawn as empty boxes) and are kept on save | | 2026-10-13 | Word's privacy options are kept and honoured when saving .docx (#1062): with Remove personal information, comments and tracked changes are saved by "Author" and the file has no creator or last-modified-by; with Remove date and time, they are saved without dates. The open document keeps the real names and dates; `file.privacyOptions` sets both | | 2026-10-13 | A selected comment balloon's Delete, Reply and Resolve buttons work again (#1085): pressing them no longer counts as a click on the page that closes the balloon first, so a new empty comment, or any other, can be deleted from its balloon | @@ -393,6 +394,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | 2026-10-13 | trivial | Progress log: `--control` checks its port (#1426) | | 2026-10-14 | trivial | Progress log: words beside a floating picture are no longer split (#1376) | | 2026-10-14 | trivial | Progress log: VML preset shapes (#1377) | +| 2026-10-14 | trivial | Progress log: Russian letter list labels | | 2026-10-12 | trivial | Progress log: VML text box margins, fit and alignment; hidden drawings | | 2026-10-13 | trivial | Progress log: comment balloon buttons work, empty comments can be deleted (#1085) | | 2026-10-12 | trivial | Progress log: sample-text formulas on Enter (#1067) | diff --git a/crates/doc/src/section.rs b/crates/doc/src/section.rs index 4f41f752b..846505119 100644 --- a/crates/doc/src/section.rs +++ b/crates/doc/src/section.rs @@ -256,7 +256,7 @@ impl NumFormat { NumFormat::ALL.into_iter().find(|f| f.ooxml() == s).unwrap_or(NumFormat::Decimal) } /// Format `n` (1-based). Formats WordCraft doesn't draw yet (Hebrew, Arabic, Hindi, Thai, - /// Vietnamese, Russian, text amounts) show decimal, as do values past a sequence's end + /// Vietnamese, text amounts) show decimal, as do values past a sequence's end /// (the 21st item of ①…⑳). pub fn format(self, n: u32) -> String { use crate::cjk_numbers as cjk; @@ -266,6 +266,8 @@ impl NumFormat { NumFormat::LowerRoman => roman(n), NumFormat::UpperLetter => letters(n).to_uppercase(), NumFormat::LowerLetter => letters(n), + NumFormat::RussianUpper => russian_letters(n).to_uppercase(), + NumFormat::RussianLower => russian_letters(n), NumFormat::Ordinal => format!("{n}{}", ordinal_suffix(n)), NumFormat::CardinalText => cardinal_text(n), NumFormat::OrdinalText => ordinal_text(n), @@ -304,8 +306,6 @@ impl NumFormat { | NumFormat::DecimalHalfWidth | NumFormat::Hex | NumFormat::VietnameseCounting - | NumFormat::RussianLower - | NumFormat::RussianUpper | NumFormat::Hebrew1 | NumFormat::Hebrew2 | NumFormat::ArabicAlpha @@ -383,6 +383,22 @@ pub fn letters(n: u32) -> String { std::iter::repeat_n(c, k as usize).collect() } +/// The 29 letters Word counts Russian lists with: а..я without ё, й, ъ and ь (ы stays). +const RUSSIAN_LETTERS: [char; 29] = + ['а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з', 'и', 'к', 'л', 'м', 'н', 'о', 'п', 'р', 'с', 'т', 'у', 'ф', 'х', 'ц', 'ч', 'ш', 'щ', 'ы', 'э', 'ю', 'я']; + +/// Word-style Russian letters: а..я, аа..яя, ааа.. (repeated letter, as [`letters`]). +pub fn russian_letters(n: u32) -> String { + if n == 0 { + return String::new(); + } + let len = RUSSIAN_LETTERS.len() as u32; + let n = n.min(len * 30); + let k = (n - 1) / len + 1; + let c = RUSSIAN_LETTERS.get(((n - 1) % len) as usize).copied().unwrap_or('а'); + std::iter::repeat_n(c, k as usize).collect() +} + const ONES: [&str; 20] = [ "zero", "one", @@ -860,6 +876,8 @@ mod tests { assert_eq!(NumFormat::ChineseCountingThousand.format(101), "一百零一"); assert_eq!(NumFormat::DecimalEnclosedCircle.format(3), "③"); assert_eq!(NumFormat::Hebrew1.format(3), "3", "formats not drawn yet show decimal"); + assert_eq!(NumFormat::RussianLower.format(0), ""); + assert_eq!(NumFormat::RussianUpper.format(u32::MAX).chars().count(), 30); for f in NumFormat::ALL { assert_eq!(NumFormat::from_ooxml(f.ooxml()), f, "{f:?} round-trips"); assert!(f == NumFormat::Bullet || f == NumFormat::None || !f.format(u32::MAX).is_empty(), "{f:?}"); @@ -867,6 +885,17 @@ mod tests { assert_eq!(NumFormat::from_ooxml("klingon"), NumFormat::Decimal, "unknown formats are decimal"); } + #[test] + fn russian_letters_follow_word() { + // Word's own labels for a russianLower list: no ё, й, ъ or ь, then doubled letters. + let labels: Vec = (1..=31).map(|n| NumFormat::RussianLower.format(n)).collect(); + assert_eq!(labels.join(" "), "а б в г д е ж з и к л м н о п р с т у ф х ц ч ш щ ы э ю я аа бб"); + assert_eq!(NumFormat::RussianLower.format(59), "ааа"); + assert_eq!(NumFormat::RussianLower.format(100), "нннн"); + assert_eq!(NumFormat::RussianUpper.format(26), "Ы"); + assert_eq!(NumFormat::RussianUpper.format(36), "ЖЖ"); + } + #[test] fn landscape_swaps() { let mut s = SectionProps { margin_left: 50.0, ..Default::default() }; diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index d0bc8ba4a..b669a5a93 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -738,6 +738,37 @@ fn lists_get_labels_and_indent() { assert!((x - 108.0).abs() < 0.5, "{x}"); } +#[test] +fn russian_letter_list_labels_draw_cyrillic() { + // russianLower labelled items 1, 2, 3 instead of а), б), в). + use wordcraft_doc::section::NumFormat; + let mut d = Document::from_text("один\nдва\nтри"); + let id = d.numbering.add_list(wordcraft_doc::ListKind::Numbered); + for a in &mut d.numbering.abstracts { + if let Some(l) = a.levels.first_mut() { + l.format = NumFormat::RussianLower; + l.text = "%1)".into(); + } + } + d.format_paragraphs(&Pos::body(0, 0), &Pos::body(2, 3), &|p| p.numbering = Some(wordcraft_doc::props::NumRef { num: id, level: 0 })).unwrap(); + let l = lay(&d); + let labels: Vec<(String, bool)> = l.pages[0] + .items + .iter() + .filter_map(|it| if let Placed::Lines { para, .. } = it { Some(para) } else { None }) + .filter_map(|pl| { + let lab = pl.label.as_ref()?; + let drawn = lab.runs.iter().all(|r| { + let face = &pl.styles[r.style as usize].face; + let gids: Vec = lab.glyphs[r.glyphs.clone()].iter().map(|g| g.gid).collect(); + r.text.chars().all(|c| gids.contains(&face.glyph_for(c)) && face.glyph_for(c) != 0) + }); + Some((lab.text.clone(), drawn)) + }) + .collect(); + assert_eq!(labels, [("а)".to_string(), true), ("б)".to_string(), true), ("в)".to_string(), true)]); +} + #[test] fn chinese_counting_list_labels_and_their_font_runs() { // Issue #534: a "一、" list showed digits, drawn with glyph ids of another font. diff --git a/docs/layout-parity.md b/docs/layout-parity.md index 985735c36..35c993737 100644 --- a/docs/layout-parity.md +++ b/docs/layout-parity.md @@ -1,6 +1,6 @@ # Layout and pagination parity with Microsoft Word -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (justified vertical page alignment, #1320; previously trivial: widow control moves a three-line paragraph whole; previously trivial: STYLEREF running headers per page, #775; previously trivial: PAGEREF in headers and footers shows its bookmark's page, #1477; previously trivial: a paragraph continued into the next column or page wraps around that column's shapes, #1374; previously trivial: PAGE fields in a paragraph split across pages show the page they are on, #1352; previously trivial: symbol-font text drawn as Unicode symbols, #776; previously trivial: large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (justified vertical page alignment, #1320; previously trivial: Russian letter list labels, а) б) в); previously trivial: a dragged floating object anchors in the column it lands in, #884; previously trivial: a paragraph ending on a page with footnotes keeps its space after above the separator, #1116; previously trivial: symbol-font text drawn as Unicode symbols, #776; previously trivial: large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 A document only "opens correctly" if its pages break where Word breaks them: the same lines, the same page count, footnotes and pictures on the same pages. This checklist covers line breaking, @@ -36,7 +36,7 @@ font can't match Word's metrics. | Drop caps | ✅ | 2026-10-06 | — | | Columns, column breaks, separators, balancing | ✅ | Columns balance on a section's last page when the next section starts continuous (also at the document end, which Word only balances when the document ends with a continuous break); not after next-page/even/odd breaks or on a page with a column break; keep-together, keep-with-next, widow/orphan control and unsplittable rows can leave them slightly uneven (#350) | — | | Section breaks (next page, continuous, even, odd), different first page, odd/even headers | ✅ | Section breaks don't show in Draft view (#42). A break inserted with text selected goes before the selection and keeps it, as in Word (#852) | 1–2 | -| List labels: number formats (decimal, letters, roman, ordinal text, Chinese/Japanese/Korean counting and legal, 一〇一 digits, 甲乙丙, ①…⑳, full-width), `%n` text such as `第%1章` / `(%1)`, legal numbering | ✅ | `doc::cjk_numbers` tests, `layout::tests::chinese_counting_list_labels_and_their_font_runs`; a label mixing faces (digit + CJK comma) is drawn run by run (#534). Hebrew, Arabic, Hindi, Thai, Vietnamese and Russian formats show decimal The label stays attached to the start of the text when the paragraph is centred or aligned to the end, as in Word (#1159). | — | +| List labels: number formats (decimal, letters, Russian letters, roman, ordinal text, Chinese/Japanese/Korean counting and legal, 一〇一 digits, 甲乙丙, ①…⑳, full-width), `%n` text such as `第%1章` / `(%1)`, legal numbering | ✅ | `doc::cjk_numbers` tests, `doc::section::tests::russian_letters_follow_word`, `layout::tests::chinese_counting_list_labels_and_their_font_runs`, `layout::tests::russian_letter_list_labels_draw_cyrillic`; a label mixing faces (digit + CJK comma) is drawn run by run (#534); Russian letters skip ё, й, ъ and ь and then double (аа, бб), as observed in Word. Hebrew, Arabic, Hindi, Thai and Vietnamese formats show decimal The label stays attached to the start of the text when the paragraph is centred or aligned to the end, as in Word (#1159). | — | | Body top below a tall header, footer pushes body up | ✅ | #138; a footer taller than the bottom margin moves the body bottom up (#453), and a header or footer linked to the previous section takes its room too (#454) | — | | Footnotes at the page bottom, endnotes at the end, note numbering | 🟡 | A long note continues onto the next page at a line boundary (its first line stays with the reference), first in that page's note area under a full-width continuation separator (#352); the document's own separator, continuation separator and continuation notice are drawn (the notice under a note that continues); on a page with footnotes a paragraph ending there keeps its space after above the separator, or its last lines go to the next page with the note (#1116); a long endnote continues on the next column or page at a line boundary (#456), and the caret finds note text on its continuation page (#457); per-section restart partial | 1–3 | | Body top below a tall header, footer pushes body up | ✅ | #138 | — | @@ -163,6 +163,7 @@ No overall parity estimate changes from these probes. | 2026-10-14 | trivial | Widow control: a three-line paragraph with room for two of its lines at the bottom of a page moves whole, as in Word (no two-and-one split leaving a widow); a paragraph continued from the previous page may still take one line along from two | | 2026-10-13 | trivial | PAGEREF in headers and footers shows its bookmark's page (#1477) | | 2026-10-14 | trivial | A word too wide for the strip beside a square-wrapped object moves past it whole instead of being split across the strip (#1376) | +| 2026-10-14 | trivial | Russian letter list formats (`russianLower`, `russianUpper`) draw а) б) в) … аа) бб) as Word does, instead of digits | | 2026-10-13 | trivial | A shape or picture dragged into another column anchors there, so the text in that column wraps around it (#884) | | 2026-10-13 | trivial | On a page with footnotes, a paragraph that ends there keeps its space after above the footnote separator; if it doesn't fit, its last line (and, with widow control, one more) goes to the next page with the note. Pages without notes are unchanged (#1116) | | 2026-10-12 | trivial | STYLEREF fields in headers and footers show each page's chapter (or other styled text); Insert › Quick Parts › Field offers STYLEREF (#775) |