hwpforge_core/
numbering.rs

1//! Numbering definitions for outline and list numbering.
2//!
3//! A [`NumberingDef`] contains up to 10 levels of [`ParaHead`] entries,
4//! each defining the number format, prefix/suffix, and display template
5//! for that outline level.
6
7use hwpforge_foundation::NumberFormatType;
8use schemars::JsonSchema;
9use serde::{Deserialize, Serialize};
10
11/// A single level definition within a numbering scheme.
12///
13/// Maps to HWPX `<hh:paraHead>` inside `<hh:numbering>`.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
15pub struct ParaHead {
16    /// Starting number for this level.
17    pub start: u32,
18    /// Outline level (1-10).
19    pub level: u32,
20    /// Number format (DIGIT, HANGUL_SYLLABLE, etc.).
21    pub num_format: NumberFormatType,
22    /// Display template with `^N` placeholder (e.g. `"^1."`, `"(^5)"`).
23    /// Empty string for levels 9 and 10 (self-closing in HWPX).
24    pub text: String,
25    /// Whether this level is checkable.
26    pub checkable: bool,
27}
28
29/// A complete numbering definition.
30///
31/// Maps to HWPX `<hh:numbering>` inside `<hh:numberings>`.
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
33pub struct NumberingDef {
34    /// Numbering ID (1-based).
35    pub id: u32,
36    /// Starting number offset.
37    pub start: u32,
38    /// Level definitions (up to 10).
39    pub levels: Vec<ParaHead>,
40}
41
42impl NumberingDef {
43    /// Creates the default 10-level outline numbering (한글 Modern default).
44    ///
45    /// Matches golden fixture `tests/fixtures/textbox.hwpx`:
46    ///
47    /// - Level 1: DIGIT `^1.` checkable=false
48    /// - Level 2: HANGUL_SYLLABLE `^2.` checkable=false
49    /// - Level 3: DIGIT `^3)` checkable=false
50    /// - Level 4: HANGUL_SYLLABLE `^4)` checkable=false
51    /// - Level 5: DIGIT `(^5)` checkable=false
52    /// - Level 6: HANGUL_SYLLABLE `(^6)` checkable=false
53    /// - Level 7: CIRCLED_DIGIT `^7` checkable=true
54    /// - Level 8: CIRCLED_HANGUL_SYLLABLE `^8` checkable=true
55    /// - Level 9: HANGUL_JAMO `` (empty) checkable=false
56    /// - Level 10: ROMAN_SMALL `` (empty) checkable=true
57    pub fn default_outline() -> Self {
58        Self {
59            id: 1,
60            start: 0,
61            levels: vec![
62                ParaHead {
63                    start: 1,
64                    level: 1,
65                    num_format: NumberFormatType::Digit,
66                    text: "^1.".into(),
67                    checkable: false,
68                },
69                ParaHead {
70                    start: 1,
71                    level: 2,
72                    num_format: NumberFormatType::HangulSyllable,
73                    text: "^2.".into(),
74                    checkable: false,
75                },
76                ParaHead {
77                    start: 1,
78                    level: 3,
79                    num_format: NumberFormatType::Digit,
80                    text: "^3)".into(),
81                    checkable: false,
82                },
83                ParaHead {
84                    start: 1,
85                    level: 4,
86                    num_format: NumberFormatType::HangulSyllable,
87                    text: "^4)".into(),
88                    checkable: false,
89                },
90                ParaHead {
91                    start: 1,
92                    level: 5,
93                    num_format: NumberFormatType::Digit,
94                    text: "(^5)".into(),
95                    checkable: false,
96                },
97                ParaHead {
98                    start: 1,
99                    level: 6,
100                    num_format: NumberFormatType::HangulSyllable,
101                    text: "(^6)".into(),
102                    checkable: false,
103                },
104                ParaHead {
105                    start: 1,
106                    level: 7,
107                    num_format: NumberFormatType::CircledDigit,
108                    text: "^7".into(),
109                    checkable: true,
110                },
111                ParaHead {
112                    start: 1,
113                    level: 8,
114                    num_format: NumberFormatType::CircledHangulSyllable,
115                    text: "^8".into(),
116                    checkable: true,
117                },
118                ParaHead {
119                    start: 1,
120                    level: 9,
121                    num_format: NumberFormatType::HangulJamo,
122                    text: String::new(),
123                    checkable: false,
124                },
125                ParaHead {
126                    start: 1,
127                    level: 10,
128                    num_format: NumberFormatType::RomanSmall,
129                    text: String::new(),
130                    checkable: true,
131                },
132            ],
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn default_outline_has_10_levels() {
143        let def = NumberingDef::default_outline();
144        assert_eq!(def.levels.len(), 10);
145        assert_eq!(def.id, 1);
146        assert_eq!(def.start, 0);
147    }
148
149    #[test]
150    fn default_outline_level_formats() {
151        let def = NumberingDef::default_outline();
152        assert_eq!(def.levels[0].num_format, NumberFormatType::Digit);
153        assert_eq!(def.levels[1].num_format, NumberFormatType::HangulSyllable);
154        assert_eq!(def.levels[6].num_format, NumberFormatType::CircledDigit);
155        assert_eq!(def.levels[7].num_format, NumberFormatType::CircledHangulSyllable);
156        assert_eq!(def.levels[8].num_format, NumberFormatType::HangulJamo);
157        assert_eq!(def.levels[9].num_format, NumberFormatType::RomanSmall);
158    }
159
160    #[test]
161    fn default_outline_level_texts() {
162        let def = NumberingDef::default_outline();
163        assert_eq!(def.levels[0].text, "^1.");
164        assert_eq!(def.levels[1].text, "^2.");
165        assert_eq!(def.levels[2].text, "^3)");
166        assert_eq!(def.levels[3].text, "^4)");
167        assert_eq!(def.levels[4].text, "(^5)");
168        assert_eq!(def.levels[5].text, "(^6)");
169        assert_eq!(def.levels[6].text, "^7");
170        assert_eq!(def.levels[7].text, "^8");
171        assert_eq!(def.levels[8].text, ""); // self-closing
172        assert_eq!(def.levels[9].text, ""); // self-closing
173    }
174
175    #[test]
176    fn default_outline_checkable_flags() {
177        let def = NumberingDef::default_outline();
178        // Levels 1-6: not checkable
179        for i in 0..6 {
180            assert!(!def.levels[i].checkable, "level {} should not be checkable", i + 1);
181        }
182        // Level 7: checkable
183        assert!(def.levels[6].checkable);
184        // Level 8: checkable
185        assert!(def.levels[7].checkable);
186        // Level 9: NOT checkable
187        assert!(!def.levels[8].checkable);
188        // Level 10: checkable
189        assert!(def.levels[9].checkable);
190    }
191
192    #[test]
193    fn default_outline_levels_are_sequential() {
194        let def = NumberingDef::default_outline();
195        for (i, lvl) in def.levels.iter().enumerate() {
196            assert_eq!(lvl.level, (i + 1) as u32);
197        }
198    }
199}