From bbaa1aa7b104df3dbf264a81ffb6a7f7c1f288b9 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 13 Oct 2026 05:27:19 -0400 Subject: [PATCH] Text: East Asian, Thai, Hindi, Arabic and Hebrew auto-number schemes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every scheme other than the Latin, Roman and circled ones drew Arabic digits ("4." for ea1ChsPeriod, "- 10 -" for arabic2Minus). Each is now written as PowerPoint's export draws it (probe deck: one box per a:buAutoNum type and start value): Chinese numerals for ea1Chs/ea1Cht (十, 十一, 二十一), digit-by-digit kanji with 〇 for ea1Jpn* (一〇), full-width digits for arabicDb*, a full-width full stop after the East Asian ones, Thai letters (without ฃ ฅ ฆ) and digits, Devanagari vowels, consonants and digits, the Arabic alphabet (arabic1Minus) and abjad (arabic2Minus) and the Hebrew alphabet with a hyphen, and the Wingdings-style circles that go to ten (circleNumWdWhite 13 → ③, circleNumWdBlack → ❶…). w04-lists s2-s4 drew wrong labels in 24 of 36 boxes. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/text/src/lib.rs | 115 +++++++++++++++++++++++++++++++++++---- crates/text/src/tests.rs | 35 ++++++++++++ 2 files changed, 138 insertions(+), 12 deletions(-) diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index dae8a6d1..20ed541d 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -550,31 +550,122 @@ fn alpha_num(n: u32, upper: bool) -> String { if upper { s.to_uppercase() } else { s } } -/// Text of an auto-number bullet: `arabicPeriod` 3 → "3.". +/// Letters of a numbering alphabet: 1 → the first, past the end the letter doubles ("aa"), as +/// the Latin alphabetic schemes do. +fn letters(list: &[char], n: u32) -> String { + let len = list.len().max(1) as u32; + let Some(i) = n.checked_sub(1) else { return String::new() }; + let c = list.get((i % len) as usize).copied().unwrap_or('?'); + std::iter::repeat_n(c, (i / len + 1).min(50) as usize).collect() +} + +/// `n` written with the digits `zero..zero+9` (Thai, Devanagari, full-width digits). +fn digits_in(n: u32, zero: char) -> String { + n.to_string().chars().map(|d| d.to_digit(10).and_then(|v| char::from_u32(zero as u32 + v)).unwrap_or(d)).collect() +} + +const CJK_DIGITS: [char; 10] = ['〇', '一', '二', '三', '四', '五', '六', '七', '八', '九']; + +/// Chinese numerals: 10 → 十, 11 → 十一, 25 → 二十五, 105 → 一百零五. +fn chinese_num(n: u32) -> String { + if n == 0 || n > 99_999 { + return n.to_string(); + } + let units = ['\0', '十', '百', '千', '万']; + let ds: Vec = n.to_string().chars().filter_map(|c| c.to_digit(10)).collect(); + let mut out = String::new(); + let mut zero = false; + for (i, d) in ds.iter().enumerate() { + let place = ds.len() - 1 - i; + if *d == 0 { + zero = true; + continue; + } + if zero { + out.push('零'); + zero = false; + } + // "十" alone for 10–19, not "一十". + if !(*d == 1 && place == 1 && i == 0) { + out.push(CJK_DIGITS.get(*d as usize).copied().unwrap_or('?')); + } + if let Some(u) = units.get(place).filter(|u| **u != '\0') { + out.push(*u); + } + } + out +} + +/// Text of an auto-number bullet: `arabicPeriod` 3 → "3.". The East Asian, Thai, Hindi, Arabic +/// and Hebrew schemes are written as PowerPoint draws them: `ea1ChsPeriod` 10 → "十.", +/// `ea1JpnKorPeriod` 10 → "一〇." (digit by digit), `arabicDbPeriod` 13 → "13.", +/// `thaiAlphaParenR` 4 → "ง)", `hindiAlpha1Period` 1 → "क.", `arabic2Minus` 10 → "ي-" +/// (abjad order), `circleNumWdWhitePlain` 13 → "③" (those circles go to ten). pub fn autonum(scheme: &str, n: u32) -> String { - let (core, upper) = if scheme.starts_with("romanUc") { - (roman(n, true), true) + const THAI: [char; 41] = [ + 'ก', 'ข', 'ค', 'ง', 'จ', 'ฉ', 'ช', 'ซ', 'ฌ', 'ญ', 'ฎ', 'ฏ', 'ฐ', 'ฑ', 'ฒ', 'ณ', 'ด', 'ต', 'ถ', 'ท', 'ธ', 'น', 'บ', 'ป', 'ผ', 'ฝ', 'พ', 'ฟ', + 'ภ', 'ม', 'ย', 'ร', 'ล', 'ว', 'ศ', 'ษ', 'ส', 'ห', 'ฬ', 'อ', 'ฮ', + ]; + const HINDI_VOWELS: [char; 11] = ['अ', 'आ', 'इ', 'ई', 'उ', 'ऊ', 'ऋ', 'ए', 'ऐ', 'ओ', 'औ']; + const HINDI_CONSONANTS: [char; 33] = [ + 'क', 'ख', 'ग', 'घ', 'ङ', 'च', 'छ', 'ज', 'झ', 'ञ', 'ट', 'ठ', 'ड', 'ढ', 'ण', 'त', 'थ', 'द', 'ध', 'न', 'प', 'फ', 'ब', 'भ', 'म', 'य', 'र', 'ल', + 'व', 'श', 'ष', 'स', 'ह', + ]; + const ARABIC_ALPHABET: [char; 28] = + ['ا', 'ب', 'ت', 'ث', 'ج', 'ح', 'خ', 'د', 'ذ', 'ر', 'ز', 'س', 'ش', 'ص', 'ض', 'ط', 'ظ', 'ع', 'غ', 'ف', 'ق', 'ك', 'ل', 'م', 'ن', 'ه', 'و', 'ي']; + const ARABIC_ABJAD: [char; 28] = + ['ا', 'ب', 'ج', 'د', 'ه', 'و', 'ز', 'ح', 'ط', 'ي', 'ك', 'ل', 'م', 'ن', 'س', 'ع', 'ف', 'ص', 'ق', 'ر', 'ش', 'ت', 'ث', 'خ', 'ذ', 'ض', 'ظ', 'غ']; + const HEBREW: [char; 22] = ['א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק', 'ר', 'ש', 'ת']; + let circled = |first: u32, count: u32| char::from_u32(first + n.saturating_sub(1) % count).map(String::from).unwrap_or_else(|| n.to_string()); + let core = if scheme.starts_with("romanUc") { + roman(n, true) } else if scheme.starts_with("romanLc") { - (roman(n, false), false) + roman(n, false) } else if scheme.starts_with("alphaUc") { - (alpha_num(n, true), true) + alpha_num(n, true) } else if scheme.starts_with("alphaLc") { - (alpha_num(n, false), false) + alpha_num(n, false) + } else if scheme.starts_with("circleNumWdWhite") { + return circled(0x2460, 10); + } else if scheme.starts_with("circleNumWdBlack") { + return circled(0x2776, 10); } else if scheme.starts_with("circleNum") { - let c = char::from_u32(0x2460 + n.saturating_sub(1).min(19)).unwrap_or('•'); - return c.to_string(); + return char::from_u32(0x2460 + n.saturating_sub(1).min(19)).map(String::from).unwrap_or_else(|| n.to_string()); + } else if scheme.starts_with("ea1Chs") || scheme.starts_with("ea1Cht") { + chinese_num(n) + } else if scheme.starts_with("ea1Jpn") { + n.to_string().chars().map(|d| d.to_digit(10).and_then(|v| CJK_DIGITS.get(v as usize).copied()).unwrap_or(d)).collect() + } else if scheme.starts_with("arabicDb") { + digits_in(n, '0') + } else if scheme.starts_with("thaiAlpha") { + letters(&THAI, n) + } else if scheme.starts_with("thaiNum") { + digits_in(n, '๐') + } else if scheme.starts_with("hindiAlpha1") { + letters(&HINDI_CONSONANTS, n) + } else if scheme.starts_with("hindiAlpha") { + letters(&HINDI_VOWELS, n) + } else if scheme.starts_with("hindiNum") { + digits_in(n, '०') + } else if scheme.starts_with("arabic1") { + letters(&ARABIC_ALPHABET, n) + } else if scheme.starts_with("arabic2") { + letters(&ARABIC_ABJAD, n) + } else if scheme.starts_with("hebrew2") { + letters(&HEBREW, n) } else { - (n.to_string(), false) + n.to_string() }; - let _ = upper; + // East Asian schemes end with a full-width full stop. + let wide = scheme.starts_with("ea1") || scheme.starts_with("arabicDb"); if scheme.ends_with("ParenBoth") { format!("({core})") } else if scheme.ends_with("ParenR") { format!("{core})") } else if scheme.ends_with("Period") { - format!("{core}.") + format!("{core}{}", if wide { '.' } else { '.' }) } else if scheme.ends_with("Minus") { - format!("- {core} -") + format!("{core}-") } else { core } diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 112e1004..04b27749 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -1028,3 +1028,38 @@ fn small_kana_and_closing_brackets_follow_powerpoint_kinsoku() { let lines = wrapped("あいうえ「。」かき", w, |p| p.hanging_punct = Some(true)); assert_eq!(lines[0], "あいうえ", "{lines:?}"); } + +#[test] +fn autonumber_schemes_follow_powerpoint() { + // As PowerPoint's export draws them (start values as in the probe deck). + for (scheme, n, want) in [ + ("ea1ChsPlain", 4, "四"), + ("ea1ChsPeriod", 10, "十."), + ("ea1ChtPeriod", 21, "二十一."), + ("ea1JpnKorPeriod", 10, "一〇."), + ("ea1JpnChsDbPeriod", 13, "一三."), + ("arabicDbPlain", 10, "10"), + ("arabicDbPeriod", 13, "13."), + ("circleNumDbPlain", 13, "⑬"), + ("circleNumWdWhitePlain", 13, "③"), + ("circleNumWdBlackPlain", 2, "❷"), + ("thaiAlphaPeriod", 1, "ก."), + ("thaiAlphaParenR", 4, "ง)"), + ("thaiAlphaParenBoth", 7, "(ช)"), + ("thaiNumPeriod", 10, "๑๐."), + ("hindiAlphaPeriod", 4, "ई."), + ("hindiAlpha1Period", 1, "क."), + ("hindiNumParenR", 13, "१३)"), + ("hebrew2Minus", 1, "א-"), + ("arabic1Minus", 13, "ش-"), + ("arabic2Minus", 10, "ي-"), + ("arabicPlain", 10, "10"), + ] { + assert_eq!(autonum(scheme, n), want, "{scheme} {n}"); + } + assert_eq!(chinese_num(105), "一百零五"); + assert_eq!(chinese_num(20), "二十"); + // Past the end of an alphabet the letter doubles, like "aa". + assert_eq!(autonum("hebrew2Minus", 23), "אא-"); + assert_eq!(autonum("thaiAlphaPeriod", 0), "."); +}