Skip to main content

rdocx_oxml/
shared.rs

1//! Shared simple types and enums used across OOXML elements.
2
3use crate::error::{OxmlError, Result};
4
5/// `ST_Jc` — Paragraph justification.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ST_Jc {
8    Start,
9    End,
10    Center,
11    Both,
12    Distribute,
13    Left,
14    Right,
15}
16
17impl ST_Jc {
18    pub fn from_str(s: &str) -> Result<Self> {
19        match s {
20            "start" | "left" => Ok(ST_Jc::Left),
21            "end" | "right" => Ok(ST_Jc::Right),
22            "center" => Ok(ST_Jc::Center),
23            // Kashida justification stretches Arabic text by elongating the
24            // connecting stroke rather than by widening spaces. Shaping that
25            // faithfully is beyond this crate, and justified is what the three
26            // values mean at the paragraph level. Rejecting them instead failed
27            // the whole document open.
28            "both" | "justify" | "lowKashida" | "mediumKashida" | "highKashida" => Ok(ST_Jc::Both),
29            "distribute" => Ok(ST_Jc::Distribute),
30            _ => Err(OxmlError::InvalidValue(format!("invalid ST_Jc: {s}"))),
31        }
32    }
33
34    pub fn to_str(self) -> &'static str {
35        match self {
36            ST_Jc::Start | ST_Jc::Left => "left",
37            ST_Jc::End | ST_Jc::Right => "right",
38            ST_Jc::Center => "center",
39            ST_Jc::Both => "both",
40            ST_Jc::Distribute => "distribute",
41        }
42    }
43}
44
45/// `ST_OnOff` — Boolean toggle, can be represented as "true"/"false", "1"/"0", or attribute absence.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ST_OnOff {
48    On,
49    Off,
50}
51
52impl ST_OnOff {
53    pub fn from_str_or_default(s: Option<&str>) -> Self {
54        match s {
55            // If the attribute is absent or empty, the element presence means "on"
56            None | Some("") | Some("true") | Some("1") | Some("on") => ST_OnOff::On,
57            Some("false") | Some("0") | Some("off") => ST_OnOff::Off,
58            Some(_) => ST_OnOff::Off,
59        }
60    }
61
62    pub fn is_on(self) -> bool {
63        self == ST_OnOff::On
64    }
65
66    pub fn to_str(self) -> &'static str {
67        match self {
68            ST_OnOff::On => "true",
69            ST_OnOff::Off => "false",
70        }
71    }
72}
73
74/// `ST_UnderlineType` — Underline styles.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum ST_Underline {
77    None,
78    Single,
79    Words,
80    Double,
81    Thick,
82    Dotted,
83    Dash,
84    DotDash,
85    DotDotDash,
86    Wave,
87}
88
89impl ST_Underline {
90    pub fn from_str(s: &str) -> Result<Self> {
91        match s {
92            "none" => Ok(ST_Underline::None),
93            "single" => Ok(ST_Underline::Single),
94            "words" => Ok(ST_Underline::Words),
95            "double" => Ok(ST_Underline::Double),
96            "thick" => Ok(ST_Underline::Thick),
97            "dotted" => Ok(ST_Underline::Dotted),
98            "dash" => Ok(ST_Underline::Dash),
99            "dotDash" => Ok(ST_Underline::DotDash),
100            "dotDotDash" => Ok(ST_Underline::DotDotDash),
101            "wave" => Ok(ST_Underline::Wave),
102            _ => Err(OxmlError::InvalidValue(format!(
103                "invalid ST_Underline: {s}"
104            ))),
105        }
106    }
107
108    pub fn to_str(self) -> &'static str {
109        match self {
110            ST_Underline::None => "none",
111            ST_Underline::Single => "single",
112            ST_Underline::Words => "words",
113            ST_Underline::Double => "double",
114            ST_Underline::Thick => "thick",
115            ST_Underline::Dotted => "dotted",
116            ST_Underline::Dash => "dash",
117            ST_Underline::DotDash => "dotDash",
118            ST_Underline::DotDotDash => "dotDotDash",
119            ST_Underline::Wave => "wave",
120        }
121    }
122}
123
124/// `ST_Border` — Border styles.
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum ST_Border {
127    None,
128    Single,
129    Thick,
130    Double,
131    Dotted,
132    Dashed,
133    DotDash,
134    DotDotDash,
135    Triple,
136    ThinThickSmallGap,
137    ThickThinSmallGap,
138    ThinThickMediumGap,
139    ThickThinMediumGap,
140    ThinThickLargeGap,
141    ThickThinLargeGap,
142    Wave,
143    DoubleWave,
144    ThreeDEmboss,
145    ThreeDEngrave,
146    Outset,
147    Inset,
148}
149
150impl ST_Border {
151    pub fn from_str(s: &str) -> Result<Self> {
152        match s {
153            "none" | "nil" => Ok(Self::None),
154            "single" => Ok(Self::Single),
155            "thick" => Ok(Self::Thick),
156            "double" => Ok(Self::Double),
157            "dotted" => Ok(Self::Dotted),
158            "dashed" => Ok(Self::Dashed),
159            "dotDash" => Ok(Self::DotDash),
160            "dotDotDash" => Ok(Self::DotDotDash),
161            "triple" => Ok(Self::Triple),
162            "thinThickSmallGap" => Ok(Self::ThinThickSmallGap),
163            "thickThinSmallGap" => Ok(Self::ThickThinSmallGap),
164            "thinThickMediumGap" => Ok(Self::ThinThickMediumGap),
165            "thickThinMediumGap" => Ok(Self::ThickThinMediumGap),
166            "thinThickLargeGap" => Ok(Self::ThinThickLargeGap),
167            "thickThinLargeGap" => Ok(Self::ThickThinLargeGap),
168            "wave" => Ok(Self::Wave),
169            "doubleWave" => Ok(Self::DoubleWave),
170            "threeDEmboss" => Ok(Self::ThreeDEmboss),
171            "threeDEngrave" => Ok(Self::ThreeDEngrave),
172            "outset" => Ok(Self::Outset),
173            "inset" => Ok(Self::Inset),
174            _ => Err(OxmlError::InvalidValue(format!("invalid ST_Border: {s}"))),
175        }
176    }
177
178    pub fn to_str(self) -> &'static str {
179        match self {
180            Self::None => "none",
181            Self::Single => "single",
182            Self::Thick => "thick",
183            Self::Double => "double",
184            Self::Dotted => "dotted",
185            Self::Dashed => "dashed",
186            Self::DotDash => "dotDash",
187            Self::DotDotDash => "dotDotDash",
188            Self::Triple => "triple",
189            Self::ThinThickSmallGap => "thinThickSmallGap",
190            Self::ThickThinSmallGap => "thickThinSmallGap",
191            Self::ThinThickMediumGap => "thinThickMediumGap",
192            Self::ThickThinMediumGap => "thickThinMediumGap",
193            Self::ThinThickLargeGap => "thinThickLargeGap",
194            Self::ThickThinLargeGap => "thickThinLargeGap",
195            Self::Wave => "wave",
196            Self::DoubleWave => "doubleWave",
197            Self::ThreeDEmboss => "threeDEmboss",
198            Self::ThreeDEngrave => "threeDEngrave",
199            Self::Outset => "outset",
200            Self::Inset => "inset",
201        }
202    }
203}
204
205/// `ST_TabJc` — Tab stop alignment type.
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum ST_TabJc {
208    Left,
209    Center,
210    Right,
211    Decimal,
212    Bar,
213    Clear,
214    Num,
215}
216
217impl ST_TabJc {
218    pub fn from_str(s: &str) -> Result<Self> {
219        match s {
220            "left" | "start" => Ok(Self::Left),
221            "center" => Ok(Self::Center),
222            "right" | "end" => Ok(Self::Right),
223            "decimal" => Ok(Self::Decimal),
224            "bar" => Ok(Self::Bar),
225            "clear" => Ok(Self::Clear),
226            "num" => Ok(Self::Num),
227            _ => Err(OxmlError::InvalidValue(format!("invalid ST_TabJc: {s}"))),
228        }
229    }
230
231    pub fn to_str(self) -> &'static str {
232        match self {
233            Self::Left => "left",
234            Self::Center => "center",
235            Self::Right => "right",
236            Self::Decimal => "decimal",
237            Self::Bar => "bar",
238            Self::Clear => "clear",
239            Self::Num => "num",
240        }
241    }
242}
243
244/// `ST_TabTlc` — Tab leader character.
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub enum ST_TabLeader {
247    None,
248    Dot,
249    Hyphen,
250    Underscore,
251    Heavy,
252    MiddleDot,
253}
254
255impl ST_TabLeader {
256    pub fn from_str(s: &str) -> Result<Self> {
257        match s {
258            "none" => Ok(Self::None),
259            "dot" => Ok(Self::Dot),
260            "hyphen" => Ok(Self::Hyphen),
261            "underscore" => Ok(Self::Underscore),
262            "heavy" => Ok(Self::Heavy),
263            "middleDot" => Ok(Self::MiddleDot),
264            _ => Err(OxmlError::InvalidValue(format!(
265                "invalid ST_TabLeader: {s}"
266            ))),
267        }
268    }
269
270    pub fn to_str(self) -> &'static str {
271        match self {
272            Self::None => "none",
273            Self::Dot => "dot",
274            Self::Hyphen => "hyphen",
275            Self::Underscore => "underscore",
276            Self::Heavy => "heavy",
277            Self::MiddleDot => "middleDot",
278        }
279    }
280}
281
282/// `ST_SectionType` — Section break type.
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
284pub enum ST_SectionType {
285    NextPage,
286    Continuous,
287    EvenPage,
288    OddPage,
289    NextColumn,
290}
291
292impl ST_SectionType {
293    pub fn from_str(s: &str) -> Result<Self> {
294        match s {
295            "nextPage" => Ok(Self::NextPage),
296            "continuous" => Ok(Self::Continuous),
297            "evenPage" => Ok(Self::EvenPage),
298            "oddPage" => Ok(Self::OddPage),
299            "nextColumn" => Ok(Self::NextColumn),
300            _ => Err(OxmlError::InvalidValue(format!(
301                "invalid ST_SectionType: {s}"
302            ))),
303        }
304    }
305
306    pub fn to_str(self) -> &'static str {
307        match self {
308            Self::NextPage => "nextPage",
309            Self::Continuous => "continuous",
310            Self::EvenPage => "evenPage",
311            Self::OddPage => "oddPage",
312            Self::NextColumn => "nextColumn",
313        }
314    }
315}
316
317/// `ST_PageOrientation` — Page orientation.
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub enum ST_PageOrientation {
320    Portrait,
321    Landscape,
322}
323
324impl ST_PageOrientation {
325    pub fn from_str(s: &str) -> Result<Self> {
326        match s {
327            "portrait" => Ok(Self::Portrait),
328            "landscape" => Ok(Self::Landscape),
329            _ => Err(OxmlError::InvalidValue(format!(
330                "invalid ST_PageOrientation: {s}"
331            ))),
332        }
333    }
334
335    pub fn to_str(self) -> &'static str {
336        match self {
337            Self::Portrait => "portrait",
338            Self::Landscape => "landscape",
339        }
340    }
341}
342
343/// `ST_HighlightColor` — Highlight colors.
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub enum ST_HighlightColor {
346    Black,
347    Blue,
348    Cyan,
349    DarkBlue,
350    DarkCyan,
351    DarkGray,
352    DarkGreen,
353    DarkMagenta,
354    DarkRed,
355    DarkYellow,
356    Green,
357    LightGray,
358    Magenta,
359    None,
360    Red,
361    White,
362    Yellow,
363}
364
365impl ST_HighlightColor {
366    pub fn from_str(s: &str) -> Result<Self> {
367        match s {
368            "black" => Ok(Self::Black),
369            "blue" => Ok(Self::Blue),
370            "cyan" => Ok(Self::Cyan),
371            "darkBlue" => Ok(Self::DarkBlue),
372            "darkCyan" => Ok(Self::DarkCyan),
373            "darkGray" => Ok(Self::DarkGray),
374            "darkGreen" => Ok(Self::DarkGreen),
375            "darkMagenta" => Ok(Self::DarkMagenta),
376            "darkRed" => Ok(Self::DarkRed),
377            "darkYellow" => Ok(Self::DarkYellow),
378            "green" => Ok(Self::Green),
379            "lightGray" => Ok(Self::LightGray),
380            "magenta" => Ok(Self::Magenta),
381            "none" => Ok(Self::None),
382            "red" => Ok(Self::Red),
383            "white" => Ok(Self::White),
384            "yellow" => Ok(Self::Yellow),
385            _ => Err(OxmlError::InvalidValue(format!(
386                "invalid ST_HighlightColor: {s}"
387            ))),
388        }
389    }
390
391    pub fn to_str(self) -> &'static str {
392        match self {
393            Self::Black => "black",
394            Self::Blue => "blue",
395            Self::Cyan => "cyan",
396            Self::DarkBlue => "darkBlue",
397            Self::DarkCyan => "darkCyan",
398            Self::DarkGray => "darkGray",
399            Self::DarkGreen => "darkGreen",
400            Self::DarkMagenta => "darkMagenta",
401            Self::DarkRed => "darkRed",
402            Self::DarkYellow => "darkYellow",
403            Self::Green => "green",
404            Self::LightGray => "lightGray",
405            Self::Magenta => "magenta",
406            Self::None => "none",
407            Self::Red => "red",
408            Self::White => "white",
409            Self::Yellow => "yellow",
410        }
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417
418    // F-X014, kashida justification values.
419
420    #[test]
421    fn kashida_justification_maps_to_both() {
422        for value in ["lowKashida", "mediumKashida", "highKashida"] {
423            assert_eq!(
424                ST_Jc::from_str(value).unwrap(),
425                ST_Jc::Both,
426                "{value} should justify"
427            );
428        }
429    }
430
431    #[test]
432    fn an_unknown_justification_is_still_rejected() {
433        // The story widens the accepted set. It does not remove the check.
434        assert!(ST_Jc::from_str("sideways").is_err());
435        assert!(ST_Jc::from_str("").is_err());
436    }
437
438    #[test]
439    fn a_document_using_kashida_justification_still_opens() {
440        use crate::document::CT_Document;
441
442        for value in ["lowKashida", "mediumKashida", "highKashida"] {
443            let xml = format!(
444                r#"<?xml version="1.0"?>
445<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
446  <w:body><w:p><w:pPr><w:jc w:val="{value}"/><w:keepNext/></w:pPr>
447  <w:r><w:t>Arabic justified text.</w:t></w:r></w:p></w:body></w:document>"#
448            );
449
450            let document = CT_Document::from_xml(xml.as_bytes())
451                .unwrap_or_else(|e| panic!("a document using {value} must open, got {e}"));
452
453            let crate::document::BodyContent::Paragraph(paragraph) = &document.body.content[0]
454            else {
455                panic!("expected a paragraph");
456            };
457            let properties = paragraph.properties.as_ref().expect("properties survive");
458            assert_eq!(properties.jc, Some(ST_Jc::Both), "{value} justifies");
459            assert_eq!(
460                properties.keep_next,
461                Some(true),
462                "{value} must not cost the paragraph its sibling properties"
463            );
464            assert_eq!(paragraph.text(), "Arabic justified text.");
465        }
466    }
467}