From c28387eaf90410abfdb3762f1749bfd21af6181c Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 10 Oct 2026 21:23:01 +0000 Subject: [PATCH] Sections: Kanji page numbers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kanji numbering style (一, 二, 三, …, written digit by digit: 10 is 一〇) for sections, footnotes, endnotes and lists. IDML PageNumberStyle and FootnoteNumberingStyle read and write Kanji and the leading-zero styles; the list format "一, 二, 三, 四..." maps to it. --- crates/doc/src/attrs.rs | 29 ++++++++++++++++++++ crates/engine/src/cmd/endnotes.rs | 2 +- crates/engine/src/cmd/layout.rs | 2 +- crates/engine/src/cmd/notes.rs | 2 +- crates/idml/src/export.rs | 2 +- crates/idml/src/names.rs | 27 ++++++++++--------- crates/idml/src/tests.rs | 44 ++++++++++++++++++++++++++++++- crates/ui-egui/src/dialogs.rs | 1 + crates/ui-egui/src/i18n.rs | 1 + crates/ui-egui/src/i18n/ar.rs | 1 + crates/ui-egui/src/i18n/it.rs | 1 + crates/ui-egui/src/i18n/ja.rs | 1 + crates/ui-egui/src/i18n/pt_br.rs | 1 + crates/ui-egui/src/i18n/uk.rs | 1 + 14 files changed, 97 insertions(+), 18 deletions(-) diff --git a/crates/doc/src/attrs.rs b/crates/doc/src/attrs.rs index b99740e2c..cb14c226f 100644 --- a/crates/doc/src/attrs.rs +++ b/crates/doc/src/attrs.rs @@ -577,6 +577,8 @@ pub enum NumberStyle { ArabicFourDigits, /// *, †, ‡, §, ¶, ‖, then doubled (**, ††, …): footnotes. Symbols, + /// 一, 二, 三, …: Chinese numerals written digit by digit, so 10 is 一〇 and 38 is 三八. + Kanji, } /// Numbers past this are written in Arabic digits by the styles that repeat characters (roman @@ -607,6 +609,7 @@ impl NumberStyle { let n = n.max(1) as usize - 1; std::iter::repeat_n(SYM[n % SYM.len()], n / SYM.len() + 1).collect() } + NumberStyle::Kanji => kanji_digits(n), } } } @@ -663,6 +666,11 @@ pub fn expand_list_text(s: &str, number: &str) -> String { out } +fn kanji_digits(n: u32) -> String { + const DIGITS: [char; 10] = ['〇', '一', '二', '三', '四', '五', '六', '七', '八', '九']; + n.to_string().chars().map(|c| c.to_digit(10).and_then(|d| DIGITS.get(d as usize)).copied().unwrap_or(c)).collect() +} + fn roman(mut n: u32) -> String { if n == 0 { return "0".into(); @@ -1133,6 +1141,27 @@ mod tests { assert_eq!(NumberStyle::Symbols.format(u32::MAX), u32::MAX.to_string()); } + #[test] + fn kanji_numbers_are_written_digit_by_digit() { + for (n, s) in [ + (0, "〇"), + (1, "一"), + (5, "五"), + (10, "一〇"), + (11, "一一"), + (20, "二〇"), + (38, "三八"), + (99, "九九"), + (100, "一〇〇"), + (105, "一〇五"), + (1000, "一〇〇〇"), + (2026, "二〇二六"), + (u32::MAX, "四二九四九六七二九五"), + ] { + assert_eq!(NumberStyle::Kanji.format(n), s, "{n}"); + } + } + #[test] fn list_labels_expand_metacharacters() { let p = ParaProps { number_style: NumberStyle::UpperLetters, ..Default::default() }; diff --git a/crates/engine/src/cmd/endnotes.rs b/crates/engine/src/cmd/endnotes.rs index bdf9ca695..f4fc4900f 100644 --- a/crates/engine/src/cmd/endnotes.rs +++ b/crates/engine/src/cmd/endnotes.rs @@ -61,7 +61,7 @@ pub fn specs() -> Vec { "Document Endnote Options…", ["Type"], None, - "{style?: arabic|upperRoman|lowerRoman|upperLetters|lowerLetters, startAt?, prefix?, suffix?, heading?, headingStyle?, paraStyle?, separator?} → the options", + "{style?: arabic|upperRoman|lowerRoman|upperLetters|lowerLetters|kanji, startAt?, prefix?, suffix?, heading?, headingStyle?, paraStyle?, separator?} → the options", has_doc, |s, p| { let cur = serde_json::to_value(&s.doc()?.doc.endnote_options).map_err(|e| bad("endnote.options", e.to_string()))?; diff --git a/crates/engine/src/cmd/layout.rs b/crates/engine/src/cmd/layout.rs index bd5320799..125470568 100644 --- a/crates/engine/src/cmd/layout.rs +++ b/crates/engine/src/cmd/layout.rs @@ -385,7 +385,7 @@ pub fn specs() -> Vec { "Numbering & Section Options…", ["Layout"], None, - "{page (1-based; starts a section there), startNumber?: n|null (continue), style?: arabic|upperRoman|lowerRoman|upperLetters|lowerLetters, prefix?, includePrefix?, marker?, remove?: bool}", + "{page (1-based; starts a section there), startNumber?: n|null (continue), style?: arabic|upperRoman|lowerRoman|upperLetters|lowerLetters|kanji, prefix?, includePrefix?, marker?, remove?: bool}", has_doc, |s, p| { let page = p.get("page").and_then(Value::as_u64).ok_or_else(|| bad("layout.section", "missing page"))? as usize; diff --git a/crates/engine/src/cmd/notes.rs b/crates/engine/src/cmd/notes.rs index 4d7b0f13f..ec4c7801f 100644 --- a/crates/engine/src/cmd/notes.rs +++ b/crates/engine/src/cmd/notes.rs @@ -22,7 +22,7 @@ pub fn specs() -> Vec { "Document Footnote Options…", ["Type"], None, - "{style?: arabic|upperRoman|lowerRoman|upperLetters|lowerLetters|symbols, startAt?, restart?: never|page|spread|section, prefix?, suffix?, affixIn?: none|reference|text|both, refPosition?: superscript|subscript|normal|otSuperscript, refCharStyle?, paraStyle?, separator?, spaceBefore?, spaceBetween?, firstBaseline?, firstBaselineMin?, spanColumns?, rule?: {on, weight, color, tint, width, offset, leftIndent}} → the options (no params: just read them)", + "{style?: arabic|upperRoman|lowerRoman|upperLetters|lowerLetters|symbols|kanji, startAt?, restart?: never|page|spread|section, prefix?, suffix?, affixIn?: none|reference|text|both, refPosition?: superscript|subscript|normal|otSuperscript, refCharStyle?, paraStyle?, separator?, spaceBefore?, spaceBetween?, firstBaseline?, firstBaselineMin?, spanColumns?, rule?: {on, weight, color, tint, width, offset, leftIndent}} → the options (no params: just read them)", has_doc, options ), diff --git a/crates/idml/src/export.rs b/crates/idml/src/export.rs index 9d73562b9..22f7f2aa9 100644 --- a/crates/idml/src/export.rs +++ b/crates/idml/src/export.rs @@ -1875,7 +1875,7 @@ impl<'a> Ex<'a> { let restart = names::NOTE_RESTART.iter().find(|r| r.0 == o.restart).map_or("DontRestart", |r| r.1); let affix = names::NOTE_AFFIX.iter().find(|r| r.0 == o.affix_in).map_or("NoPrefixSuffix", |r| r.1); let mut props = El::new("Properties"); - props.push(enumv("FootnoteNumberingStyle", names::note_style_out(o.style))); + props.push(enumv("FootnoteNumberingStyle", names::number_style_out(o.style))); props.push(enumv("RestartNumbering", restart)); props.push(enumv("ShowPrefixSuffix", affix)); props.push(enumv("MarkerPositioning", names::note_marker_out(o.ref_position))); diff --git a/crates/idml/src/names.rs b/crates/idml/src/names.rs index 1df0f56d5..6703b9069 100644 --- a/crates/idml/src/names.rs +++ b/crates/idml/src/names.rs @@ -460,23 +460,32 @@ pub const REF_POINTS: [&str; 9] = [ "BottomRightPoint", ]; +/// IDML `PageNumberStyle` of a section, also written for `FootnoteNumberingStyle`. pub fn number_style_out(n: NumberStyle) -> &'static str { match n { - NumberStyle::Arabic | NumberStyle::ArabicLeadingZero | NumberStyle::ArabicThreeDigits | NumberStyle::ArabicFourDigits => "Arabic", + NumberStyle::Arabic => "Arabic", + NumberStyle::ArabicLeadingZero => "SingleLeadingZeros", + NumberStyle::ArabicThreeDigits => "DoubleLeadingZeros", + NumberStyle::ArabicFourDigits => "TripleLeadingZeros", NumberStyle::UpperRoman => "UpperRoman", NumberStyle::LowerRoman => "LowerRoman", NumberStyle::UpperLetters => "UpperLetters", NumberStyle::LowerLetters => "LowerLetters", NumberStyle::Symbols => "Symbols", + NumberStyle::Kanji => "Kanji", } } pub fn number_style_in(s: &str) -> NumberStyle { match s { + "SingleLeadingZeros" => NumberStyle::ArabicLeadingZero, + "DoubleLeadingZeros" => NumberStyle::ArabicThreeDigits, + "TripleLeadingZeros" => NumberStyle::ArabicFourDigits, "UpperRoman" => NumberStyle::UpperRoman, "LowerRoman" => NumberStyle::LowerRoman, "UpperLetters" => NumberStyle::UpperLetters, "LowerLetters" => NumberStyle::LowerLetters, "Symbols" => NumberStyle::Symbols, + "Kanji" => NumberStyle::Kanji, _ => NumberStyle::Arabic, } } @@ -544,7 +553,7 @@ pub fn pt(x: f64, y: f64) -> String { // ---------- list numbering ---------- /// A paragraph's `NumberingFormat` ("A, B, C, D...", "001, 002, 003...", …), told apart by its -/// first item. None for formats with no counterpart (CJK, Arabic, Hebrew, …). +/// first item. None for formats with no counterpart (other CJK forms, Arabic, Hebrew, …). pub fn numbering_format_in(s: &str) -> Option { Some(match s.split(',').next().unwrap_or("").trim() { "1" => NumberStyle::Arabic, @@ -555,6 +564,7 @@ pub fn numbering_format_in(s: &str) -> Option { "i" => NumberStyle::LowerRoman, "A" => NumberStyle::UpperLetters, "a" => NumberStyle::LowerLetters, + "一" => NumberStyle::Kanji, _ => return None, }) } @@ -568,6 +578,7 @@ pub fn numbering_format_out(n: NumberStyle) -> &'static str { NumberStyle::LowerRoman => "i, ii, iii, iv...", NumberStyle::UpperLetters => "A, B, C, D...", NumberStyle::LowerLetters => "a, b, c, d...", + NumberStyle::Kanji => "一, 二, 三, 四...", } } /// Label text in IDML metacharacters (a tab is `^t`). @@ -577,19 +588,9 @@ pub fn list_text_out(s: &str) -> String { // ---------- footnote options ---------- -pub fn note_style_out(n: NumberStyle) -> &'static str { - match n { - NumberStyle::ArabicLeadingZero => "SingleLeadingZeros", - NumberStyle::ArabicThreeDigits => "DoubleLeadingZeros", - NumberStyle::ArabicFourDigits => "TripleLeadingZeros", - n => number_style_out(n), - } -} +/// IDML `FootnoteNumberingStyle`: the page number styles and `Asterisks`. pub fn note_style_in(s: &str) -> NumberStyle { match s { - "SingleLeadingZeros" => NumberStyle::ArabicLeadingZero, - "DoubleLeadingZeros" => NumberStyle::ArabicThreeDigits, - "TripleLeadingZeros" => NumberStyle::ArabicFourDigits, "Asterisks" => NumberStyle::Symbols, s => number_style_in(s), } diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 7b747587a..01dbdb50e 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -239,6 +239,48 @@ fn odd_page_break_ends_its_paragraph_and_keeps_its_parity() { assert_eq!(cs.frames[1].lines.iter().map(|l| l.para).collect::>(), vec![1]); } +/// A section numbered in Kanji: page names, the folio a page-number marker draws, and a round trip. +#[test] +fn kanji_page_numbers() { + let designmap = DESIGNMAP.replace(">LowerRoman<", ">Kanji<"); + let bytes = zip_files(&[ + ("designmap.xml", &designmap), + ("Resources/Graphic.xml", GRAPHIC), + ("Resources/Styles.xml", STYLES), + ("Resources/Preferences.xml", PREFS), + ("MasterSpreads/MasterSpread_m1.xml", MASTER), + ("Spreads/Spread_sp1.xml", SPREAD), + ("Stories/Story_s1.xml", STORY), + ]); + let mut d = import_idml(&bytes).unwrap(); + assert_eq!(d.sections[0].style, designcraft_doc::NumberStyle::Kanji); + assert_eq!((d.page_name(0), d.page_name(1)), ("五".to_string(), "六".to_string())); + let back = import_idml(&export_idml(&d)).unwrap(); + assert_eq!(back.sections[0].style, designcraft_doc::NumberStyle::Kanji); + assert_eq!(back.page_name(1), "六"); + + // The marker draws the same glyphs as the page name typed in its place. + let sid = *d.stories.keys().next().unwrap(); + let at = d.story(sid).unwrap().text.find(st::PAGE_NUMBER).unwrap(); + let marker = |d: &Document| { + let cs = designcraft_compose::compose_story(d, sid, &Default::default()); + cs.frames + .iter() + .enumerate() + .find_map(|(fi, f)| { + let gids: Vec = f.lines.iter().flat_map(|l| &l.glyphs).filter(|g| g.byte == at).map(|g| g.gid).collect(); + (!gids.is_empty()).then_some((fi, gids)) + }) + .unwrap() + }; + let (frame, folio) = marker(&d); + let name = d.page_name(frame); + assert_eq!(name.len(), st::PAGE_NUMBER.len_utf8(), "same byte length keeps the story's ranges"); + let story = d.story_mut(sid).unwrap(); + story.text.replace_range(at..at + name.len(), &name); + assert_eq!(marker(&d), (frame, folio)); +} + #[test] fn imports_hand_written_fixture() { let d = import_idml_with(&fixture(), &|_| None).unwrap(); @@ -1391,7 +1433,7 @@ fn list_numbering_format_expression_and_bullet_import_and_round_trip() { let (table, _) = d.styles.resolve_para_style("Text/Table"); assert_eq!(table.number_label(7), "Tabel 007\t"); let (kanji, _) = d.styles.resolve_para_style("Text/Kanji"); - assert_eq!(kanji.number_style, designcraft_doc::NumberStyle::Arabic); + assert_eq!(kanji.number_style, designcraft_doc::NumberStyle::Kanji); let (point, _) = d.styles.resolve_para_style("Text/Point"); assert_eq!(point.bullet_label(), "\u{25A0}\u{2002}"); }; diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 2ab7a3bad..8658f3926 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -3373,6 +3373,7 @@ fn footnote_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { ("lowerLetters", "a, b, c, d..."), ("arabicLeadingZero", "01, 02, 03..."), ("symbols", "*, †, ‡, §..."), + ("kanji", "一, 二, 三, 四..."), ], ); ui.end_row(); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index e5e8a8d08..c2cd8a7a5 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -2378,6 +2378,7 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Linear", ["Linear", "Linear", "Linear", "Linear", "线性"]), ("Radial", ["Radial", "Radial", "Radial", "Radial", "径向"]), ("Arabic", ["Arabic", "Arabic", "Arabic", "Arabic", "阿拉伯语"]), + ("Kanji", ["Kanji", "Kanji", "Kanji", "漢数字", "汉字数字"]), ("Beveled Rectangle", ["Beveled Rectangle", "Beveled Rectangle", "Beveled Rectangle", "Beveled Rectangle", "斜角矩形"]), ("Chinese", ["Chinese", "Chinese", "Chinese", "Chinese", "中文"]), ( diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index 10e393cd2..d49a91d76 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -640,6 +640,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Linear", "خطي"), ("Radial", "شعاعي"), ("Arabic", "العربية"), + ("Kanji", "أرقام كانجي"), ("Beveled Rectangle", "مستطيل مشطوف"), ("Chinese", "الصينية"), ("Constrain proportions for width and height", "تقييد نسب العرض والارتفاع"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index c91afd6ba..0fd6c4e4f 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -1208,6 +1208,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Linear", "Lineare"), ("Radial", "Radiale"), ("Arabic", "Arabo"), + ("Kanji", "Kanji"), ("Beveled Rectangle", "Rettangolo smussato"), ("Chinese", "Cinese"), ("Constrain proportions for width and height", "Vincola proporzioni di larghezza e altezza"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index be6e2c4da..ab603ae6a 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -1207,6 +1207,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Linear", "線形"), ("Radial", "放射状"), ("Arabic", "アラビア語"), + ("Kanji", "漢数字"), ("Beveled Rectangle", "角を面取りした長方形"), ("Chinese", "中国語"), ("Constrain proportions for width and height", "幅と高さの比率を固定"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index d25717dc6..9f0a7dcfd 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -639,6 +639,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Linear", "Linear"), ("Radial", "Radial"), ("Arabic", "Árabe"), + ("Kanji", "Kanji"), ("Beveled Rectangle", "Retângulo chanfrado"), ("Chinese", "Chinês"), ("Constrain proportions for width and height", "Restringir proporções entre largura e altura"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index 9a9fb89ba..4b43685b6 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -1205,6 +1205,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Linear", "Лінійний"), ("Radial", "Радіальний"), ("Arabic", "Арабська"), + ("Kanji", "Кандзі"), ("Beveled Rectangle", "Прямокутник зі скошеними кутами"), ("Chinese", "Китайська"), ("Constrain proportions for width and height", "Зберігати співвідношення ширини та висоти"),