1use hwpforge_foundation::{HwpUnit, TabAlign, TabLeader};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
11pub struct TabStop {
12 pub position: HwpUnit,
14 pub align: TabAlign,
16 pub leader: TabLeader,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
24pub struct TabDef {
25 pub id: u32,
27 pub auto_tab_left: bool,
29 pub auto_tab_right: bool,
31 #[serde(default, skip_serializing_if = "Vec::is_empty")]
33 pub stops: Vec<TabStop>,
34}
35
36impl TabDef {
37 pub const BUILTIN_COUNT: u32 = 3;
39 pub const FIRST_CUSTOM_ID: u32 = Self::BUILTIN_COUNT;
41
42 pub fn defaults() -> [Self; 3] {
50 [
51 Self { id: 0, auto_tab_left: false, auto_tab_right: false, stops: Vec::new() },
52 Self { id: 1, auto_tab_left: true, auto_tab_right: false, stops: Vec::new() },
53 Self { id: 2, auto_tab_left: false, auto_tab_right: true, stops: Vec::new() },
54 ]
55 }
56
57 pub fn is_builtin_id(id: u32) -> bool {
59 id < Self::BUILTIN_COUNT
60 }
61
62 pub fn is_custom_id(id: u32) -> bool {
64 id >= Self::FIRST_CUSTOM_ID
65 }
66
67 pub fn merged_with_defaults<'a>(tabs: impl IntoIterator<Item = &'a Self>) -> Vec<Self> {
72 let mut merged = Self::defaults().to_vec();
73 for tab in tabs {
74 if let Some(existing) = merged.iter_mut().find(|candidate| candidate.id == tab.id) {
75 *existing = tab.clone();
76 } else {
77 merged.push(tab.clone());
78 }
79 }
80 merged.sort_by_key(|tab| tab.id);
81 merged
82 }
83
84 pub fn reference_is_known(id: u32, known_custom_ids: impl IntoIterator<Item = u32>) -> bool {
87 Self::is_builtin_id(id) || known_custom_ids.into_iter().any(|candidate| candidate == id)
88 }
89
90 pub fn clamp_position_from_unsigned(raw: u64) -> HwpUnit {
92 let clamped = raw.min(HwpUnit::MAX_VALUE as u64) as i32;
93 HwpUnit::new(clamped).expect("tab stop positions must clamp into valid HwpUnit range")
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn defaults_has_3_entries() {
103 let tabs = TabDef::defaults();
104 assert_eq!(tabs.len(), 3);
105 }
106
107 #[test]
108 fn defaults_ids_sequential() {
109 let tabs = TabDef::defaults();
110 assert_eq!(tabs[0].id, 0);
111 assert_eq!(tabs[1].id, 1);
112 assert_eq!(tabs[2].id, 2);
113 }
114
115 #[test]
116 fn defaults_auto_tab_values() {
117 let tabs = TabDef::defaults();
118 assert!(!tabs[0].auto_tab_left);
120 assert!(!tabs[0].auto_tab_right);
121 assert!(tabs[0].stops.is_empty());
122 assert!(tabs[1].auto_tab_left);
124 assert!(!tabs[1].auto_tab_right);
125 assert!(tabs[1].stops.is_empty());
126 assert!(!tabs[2].auto_tab_left);
128 assert!(tabs[2].auto_tab_right);
129 assert!(tabs[2].stops.is_empty());
130 }
131
132 #[test]
133 fn builtin_id_helpers_match_defaults_boundary() {
134 assert!(TabDef::is_builtin_id(0));
135 assert!(TabDef::is_builtin_id(2));
136 assert!(!TabDef::is_builtin_id(3));
137 assert!(!TabDef::is_custom_id(2));
138 assert!(TabDef::is_custom_id(3));
139 }
140
141 #[test]
142 fn tab_stop_preserves_position_and_semantics() {
143 let stop = TabStop {
144 position: HwpUnit::new(8000).unwrap(),
145 align: TabAlign::Decimal,
146 leader: TabLeader::dot(),
147 };
148 assert_eq!(stop.position, HwpUnit::new(8000).unwrap());
149 assert_eq!(stop.align, TabAlign::Decimal);
150 assert_eq!(stop.leader.as_hwpx_str(), "DOT");
151 }
152
153 #[test]
154 fn merged_with_defaults_keeps_builtins_and_appends_customs() {
155 let tabs = vec![
156 TabDef {
157 id: 1,
158 auto_tab_left: false,
159 auto_tab_right: false,
160 stops: vec![TabStop {
161 position: HwpUnit::new(5000).unwrap(),
162 align: TabAlign::Right,
163 leader: TabLeader::from_hwpx_str("DASH"),
164 }],
165 },
166 TabDef {
167 id: 3,
168 auto_tab_left: false,
169 auto_tab_right: false,
170 stops: vec![TabStop {
171 position: HwpUnit::new(7500).unwrap(),
172 align: TabAlign::Left,
173 leader: TabLeader::none(),
174 }],
175 },
176 ];
177
178 let merged = TabDef::merged_with_defaults(&tabs);
179
180 assert_eq!(merged.len(), 4);
181 assert_eq!(merged[0].id, 0);
182 assert_eq!(merged[1].id, 1);
183 assert_eq!(merged[2].id, 2);
184 assert_eq!(merged[3].id, 3);
185 assert_eq!(merged[1].stops.len(), 1);
186 assert_eq!(merged[3].stops.len(), 1);
187 }
188
189 #[test]
190 fn reference_is_known_accepts_builtin_and_custom_ids() {
191 assert!(TabDef::reference_is_known(0, []));
192 assert!(TabDef::reference_is_known(3, [3, 5]));
193 assert!(!TabDef::reference_is_known(4, [3, 5]));
194 }
195
196 #[test]
197 fn clamp_position_from_unsigned_caps_large_values() {
198 let clamped = TabDef::clamp_position_from_unsigned(u64::MAX);
199 assert_eq!(clamped, HwpUnit::new(HwpUnit::MAX_VALUE).unwrap());
200 }
201}