Merge kanji-page-numbers: Kanji page numbers

Conflicts: engine layout.rs and notes.rs params docs (next's lists plus kanji).
This commit is contained in:
aiongg
2026-10-10 21:27:19 +00:00
14 changed files with 97 additions and 18 deletions
+29
View File
@@ -793,6 +793,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
@@ -823,6 +825,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),
}
}
}
@@ -892,6 +895,11 @@ pub fn expand_list_levels(s: &str, number: &str, levels: &[String]) -> 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();
@@ -1631,6 +1639,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() };
+1 -1
View File
@@ -61,7 +61,7 @@ pub fn specs() -> Vec<CommandSpec> {
"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()))?;
+1 -1
View File
@@ -400,7 +400,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Numbering & Section Options…",
["Layout"],
None,
"{page (1-based; starts a section there), startNumber?: n|null (continue), style?: arabic|upperRoman|lowerRoman|upperLetters|lowerLetters|arabicLeadingZero|arabicThreeDigits|arabicFourDigits, prefix? (up to 8 characters; no + or comma), includePrefix?, marker?, remove?: bool}",
"{page (1-based; starts a section there), startNumber?: n|null (continue), style?: arabic|upperRoman|lowerRoman|upperLetters|lowerLetters|arabicLeadingZero|arabicThreeDigits|arabicFourDigits|kanji, prefix? (up to 8 characters; no + or comma), 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;
+1 -1
View File
@@ -22,7 +22,7 @@ pub fn specs() -> Vec<CommandSpec> {
"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?: ascent|capHeight|leading|xHeight|emboxHeight|fixed, 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?: ascent|capHeight|leading|xHeight|emboxHeight|fixed, firstBaselineMin?, spanColumns?, rule?: {on, weight, color, tint, width, offset, leftIndent}} → the options (no params: just read them)",
has_doc,
options
),
+1 -1
View File
@@ -2020,7 +2020,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)));
+14 -13
View File
@@ -548,23 +548,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,
}
}
@@ -632,7 +641,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<NumberStyle> {
Some(match s.split(',').next().unwrap_or("").trim() {
"1" => NumberStyle::Arabic,
@@ -643,6 +652,7 @@ pub fn numbering_format_in(s: &str) -> Option<NumberStyle> {
"i" => NumberStyle::LowerRoman,
"A" => NumberStyle::UpperLetters,
"a" => NumberStyle::LowerLetters,
"一" => NumberStyle::Kanji,
_ => return None,
})
}
@@ -656,6 +666,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`).
@@ -665,19 +676,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),
}
+43 -1
View File
@@ -628,6 +628,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<_>>(), 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<u32> = 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();
@@ -2332,7 +2374,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}");
};
+1
View File
@@ -4457,6 +4457,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();
+1
View File
@@ -2704,6 +2704,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", "中文"]),
(
+1
View File
@@ -650,6 +650,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Linear", "خطي"),
("Radial", "شعاعي"),
("Arabic", "العربية"),
("Kanji", "أرقام كانجي"),
("Beveled Rectangle", "مستطيل مشطوف"),
("Chinese", "الصينية"),
("Constrain proportions for width and height", "تقييد نسب العرض والارتفاع"),
+1
View File
@@ -1214,6 +1214,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"),
+1
View File
@@ -1330,6 +1330,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Linear", "線形"),
("Radial", "放射状"),
("Arabic", "アラビア語"),
("Kanji", "漢数字"),
("Beveled Rectangle", "角を面取りした長方形"),
("Chinese", "中国語"),
("Constrain proportions for width and height", "幅と高さの比率を固定"),
+1
View File
@@ -641,6 +641,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"),
+1
View File
@@ -1210,6 +1210,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
("Linear", "Лінійний"),
("Radial", "Радіальний"),
("Arabic", "Арабська"),
("Kanji", "Кандзі"),
("Beveled Rectangle", "Прямокутник зі скошеними кутами"),
("Chinese", "Китайська"),
("Constrain proportions for width and height", "Зберігати співвідношення ширини та висоти"),