Skip to main content

ppt_rs/generator/text/
rtl.rs

1//! RTL (right-to-left) text support
2//!
3//! Provides text direction control for RTL languages like Arabic, Hebrew, Urdu, etc.
4
5/// Text direction for paragraphs and runs
6#[derive(Clone, Debug, Copy, PartialEq, Eq, Default)]
7pub enum TextDirection {
8    #[default]
9    LTR,
10    RTL,
11}
12
13impl TextDirection {
14    /// Get the OOXML attribute value
15    pub fn to_xml_attr(&self) -> &'static str {
16        match self {
17            TextDirection::LTR => "0",
18            TextDirection::RTL => "1",
19        }
20    }
21
22    /// Whether this is RTL
23    pub fn is_rtl(&self) -> bool {
24        matches!(self, TextDirection::RTL)
25    }
26}
27
28/// RTL language presets with appropriate default fonts
29#[derive(Clone, Debug, Copy, PartialEq, Eq)]
30pub enum RtlLanguage {
31    Arabic,
32    Hebrew,
33    Urdu,
34    Persian,
35    Pashto,
36    Sindhi,
37    Kurdish,
38    Yiddish,
39}
40
41impl RtlLanguage {
42    /// Get the BCP 47 language tag
43    pub fn lang_tag(&self) -> &'static str {
44        match self {
45            RtlLanguage::Arabic => "ar-SA",
46            RtlLanguage::Hebrew => "he-IL",
47            RtlLanguage::Urdu => "ur-PK",
48            RtlLanguage::Persian => "fa-IR",
49            RtlLanguage::Pashto => "ps-AF",
50            RtlLanguage::Sindhi => "sd-PK",
51            RtlLanguage::Kurdish => "ku-IQ",
52            RtlLanguage::Yiddish => "yi-001",
53        }
54    }
55
56    /// Get the recommended default font for this language
57    pub fn default_font(&self) -> &'static str {
58        match self {
59            RtlLanguage::Arabic | RtlLanguage::Urdu | RtlLanguage::Persian
60            | RtlLanguage::Pashto | RtlLanguage::Sindhi | RtlLanguage::Kurdish => "Arial",
61            RtlLanguage::Hebrew | RtlLanguage::Yiddish => "Arial",
62        }
63    }
64
65    /// Get the text direction (always RTL for these languages)
66    pub fn direction(&self) -> TextDirection {
67        TextDirection::RTL
68    }
69}
70
71/// RTL text properties for XML generation
72#[derive(Clone, Debug, Default)]
73pub struct RtlTextProps {
74    pub direction: TextDirection,
75    pub language: Option<String>,
76    pub font_complex_script: Option<String>,
77}
78
79impl RtlTextProps {
80    pub fn new() -> Self {
81        Self::default()
82    }
83
84    /// Set RTL direction
85    pub fn rtl(mut self) -> Self {
86        self.direction = TextDirection::RTL;
87        self
88    }
89
90    /// Set LTR direction
91    pub fn ltr(mut self) -> Self {
92        self.direction = TextDirection::LTR;
93        self
94    }
95
96    /// Set language tag (e.g., "ar-SA", "he-IL")
97    pub fn language(mut self, lang: &str) -> Self {
98        self.language = Some(lang.to_string());
99        self
100    }
101
102    /// Set from an RTL language preset
103    pub fn from_language(mut self, lang: RtlLanguage) -> Self {
104        self.direction = lang.direction();
105        self.language = Some(lang.lang_tag().to_string());
106        self.font_complex_script = Some(lang.default_font().to_string());
107        self
108    }
109
110    /// Set complex script font (used for RTL rendering)
111    pub fn complex_script_font(mut self, font: &str) -> Self {
112        self.font_complex_script = Some(font.to_string());
113        self
114    }
115
116    /// Generate paragraph property XML attributes for RTL
117    pub fn to_ppr_xml_attr(&self) -> String {
118        if self.direction.is_rtl() {
119            r#" rtl="1""#.to_string()
120        } else {
121            String::new()
122        }
123    }
124
125    /// Generate run property XML attributes for RTL
126    pub fn to_rpr_xml_attrs(&self) -> String {
127        let mut attrs = String::new();
128        if let Some(ref lang) = self.language {
129            attrs.push_str(&format!(r#" lang="{lang}""#));
130        }
131        attrs
132    }
133
134    /// Generate complex script font XML element
135    pub fn to_cs_font_xml(&self) -> String {
136        if let Some(ref font) = self.font_complex_script {
137            format!(r#"<a:cs typeface="{}"/>"#, super::escape_xml(font))
138        } else {
139            String::new()
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_text_direction_default() {
150        let dir = TextDirection::default();
151        assert_eq!(dir, TextDirection::LTR);
152        assert!(!dir.is_rtl());
153    }
154
155    #[test]
156    fn test_text_direction_rtl() {
157        let dir = TextDirection::RTL;
158        assert!(dir.is_rtl());
159        assert_eq!(dir.to_xml_attr(), "1");
160    }
161
162    #[test]
163    fn test_text_direction_ltr() {
164        let dir = TextDirection::LTR;
165        assert!(!dir.is_rtl());
166        assert_eq!(dir.to_xml_attr(), "0");
167    }
168
169    #[test]
170    fn test_rtl_language_arabic() {
171        let lang = RtlLanguage::Arabic;
172        assert_eq!(lang.lang_tag(), "ar-SA");
173        assert_eq!(lang.default_font(), "Arial");
174        assert_eq!(lang.direction(), TextDirection::RTL);
175    }
176
177    #[test]
178    fn test_rtl_language_hebrew() {
179        let lang = RtlLanguage::Hebrew;
180        assert_eq!(lang.lang_tag(), "he-IL");
181        assert_eq!(lang.default_font(), "Arial");
182    }
183
184    #[test]
185    fn test_rtl_language_all_variants() {
186        let languages = [
187            RtlLanguage::Arabic, RtlLanguage::Hebrew, RtlLanguage::Urdu,
188            RtlLanguage::Persian, RtlLanguage::Pashto, RtlLanguage::Sindhi,
189            RtlLanguage::Kurdish, RtlLanguage::Yiddish,
190        ];
191        for lang in &languages {
192            assert_eq!(lang.direction(), TextDirection::RTL);
193            assert!(!lang.lang_tag().is_empty());
194            assert!(!lang.default_font().is_empty());
195        }
196    }
197
198    #[test]
199    fn test_rtl_text_props_default() {
200        let props = RtlTextProps::new();
201        assert_eq!(props.direction, TextDirection::LTR);
202        assert!(props.language.is_none());
203        assert!(props.font_complex_script.is_none());
204    }
205
206    #[test]
207    fn test_rtl_text_props_builder() {
208        let props = RtlTextProps::new()
209            .rtl()
210            .language("ar-SA")
211            .complex_script_font("Arial");
212        assert!(props.direction.is_rtl());
213        assert_eq!(props.language.as_deref(), Some("ar-SA"));
214        assert_eq!(props.font_complex_script.as_deref(), Some("Arial"));
215    }
216
217    #[test]
218    fn test_rtl_text_props_from_language() {
219        let props = RtlTextProps::new().from_language(RtlLanguage::Arabic);
220        assert!(props.direction.is_rtl());
221        assert_eq!(props.language.as_deref(), Some("ar-SA"));
222        assert_eq!(props.font_complex_script.as_deref(), Some("Arial"));
223    }
224
225    #[test]
226    fn test_rtl_ppr_xml_attr() {
227        let rtl_props = RtlTextProps::new().rtl();
228        assert_eq!(rtl_props.to_ppr_xml_attr(), r#" rtl="1""#);
229
230        let ltr_props = RtlTextProps::new().ltr();
231        assert_eq!(ltr_props.to_ppr_xml_attr(), "");
232    }
233
234    #[test]
235    fn test_rtl_rpr_xml_attrs() {
236        let props = RtlTextProps::new().language("he-IL");
237        assert!(props.to_rpr_xml_attrs().contains(r#"lang="he-IL""#));
238    }
239
240    #[test]
241    fn test_rtl_cs_font_xml() {
242        let props = RtlTextProps::new().complex_script_font("Arial");
243        let xml = props.to_cs_font_xml();
244        assert!(xml.contains(r#"<a:cs typeface="Arial"/>"#));
245
246        let empty = RtlTextProps::new();
247        assert_eq!(empty.to_cs_font_xml(), "");
248    }
249
250    #[test]
251    fn test_rtl_props_ltr_override() {
252        let props = RtlTextProps::new().rtl().ltr();
253        assert!(!props.direction.is_rtl());
254    }
255}