Skip to main content

hwpforge_foundation/
enums.rs

1//! Core enums used throughout HWP document processing.
2//!
3//! All enums are `#[non_exhaustive]` to allow future variant additions
4//! without breaking downstream code. They use `#[repr(u8)]` for compact
5//! storage and provide `TryFrom<u8>` for binary parsing.
6//!
7//! # Examples
8//!
9//! ```
10//! use hwpforge_foundation::Alignment;
11//! use std::str::FromStr;
12//!
13//! let a = Alignment::from_str("Justify").unwrap();
14//! assert_eq!(a, Alignment::Justify);
15//! assert_eq!(a.to_string(), "Justify");
16//! ```
17
18use std::fmt;
19
20use serde::{Deserialize, Serialize};
21
22use crate::error::FoundationError;
23
24// ---------------------------------------------------------------------------
25// Alignment
26// ---------------------------------------------------------------------------
27
28/// Horizontal text alignment within a paragraph.
29///
30/// # Examples
31///
32/// ```
33/// use hwpforge_foundation::Alignment;
34///
35/// assert_eq!(Alignment::default(), Alignment::Left);
36/// ```
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
38#[non_exhaustive]
39#[repr(u8)]
40pub enum Alignment {
41    /// Left-aligned (default).
42    #[default]
43    Left = 0,
44    /// Centered.
45    Center = 1,
46    /// Right-aligned.
47    Right = 2,
48    /// Justified (both edges flush).
49    Justify = 3,
50    /// Distribute spacing evenly between characters.
51    Distribute = 4,
52    /// Distribute spacing evenly between characters, last line flush.
53    DistributeFlush = 5,
54}
55
56impl fmt::Display for Alignment {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self {
59            Self::Left => f.write_str("Left"),
60            Self::Center => f.write_str("Center"),
61            Self::Right => f.write_str("Right"),
62            Self::Justify => f.write_str("Justify"),
63            Self::Distribute => f.write_str("Distribute"),
64            Self::DistributeFlush => f.write_str("DistributeFlush"),
65        }
66    }
67}
68
69impl std::str::FromStr for Alignment {
70    type Err = FoundationError;
71
72    fn from_str(s: &str) -> Result<Self, Self::Err> {
73        match s {
74            "Left" | "left" => Ok(Self::Left),
75            "Center" | "center" => Ok(Self::Center),
76            "Right" | "right" => Ok(Self::Right),
77            "Justify" | "justify" => Ok(Self::Justify),
78            "Distribute" | "distribute" => Ok(Self::Distribute),
79            "DistributeFlush" | "distributeflush" | "distribute_flush" => Ok(Self::DistributeFlush),
80            _ => Err(FoundationError::ParseError {
81                type_name: "Alignment".to_string(),
82                value: s.to_string(),
83                valid_values: "Left, Center, Right, Justify, Distribute, DistributeFlush"
84                    .to_string(),
85            }),
86        }
87    }
88}
89
90impl TryFrom<u8> for Alignment {
91    type Error = FoundationError;
92
93    fn try_from(value: u8) -> Result<Self, Self::Error> {
94        match value {
95            0 => Ok(Self::Left),
96            1 => Ok(Self::Center),
97            2 => Ok(Self::Right),
98            3 => Ok(Self::Justify),
99            4 => Ok(Self::Distribute),
100            5 => Ok(Self::DistributeFlush),
101            _ => Err(FoundationError::ParseError {
102                type_name: "Alignment".to_string(),
103                value: value.to_string(),
104                valid_values:
105                    "0 (Left), 1 (Center), 2 (Right), 3 (Justify), 4 (Distribute), 5 (DistributeFlush)"
106                        .to_string(),
107            }),
108        }
109    }
110}
111
112impl schemars::JsonSchema for Alignment {
113    fn schema_name() -> std::borrow::Cow<'static, str> {
114        std::borrow::Cow::Borrowed("Alignment")
115    }
116
117    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
118        gen.subschema_for::<String>()
119    }
120}
121
122// ---------------------------------------------------------------------------
123// LineSpacingType
124// ---------------------------------------------------------------------------
125
126/// How line spacing is calculated.
127///
128/// # Examples
129///
130/// ```
131/// use hwpforge_foundation::LineSpacingType;
132///
133/// assert_eq!(LineSpacingType::default(), LineSpacingType::Percentage);
134/// ```
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
136#[non_exhaustive]
137#[repr(u8)]
138pub enum LineSpacingType {
139    /// Spacing as a percentage of the font size (default: 160%).
140    #[default]
141    Percentage = 0,
142    /// Fixed spacing in HwpUnit, regardless of font size.
143    Fixed = 1,
144    /// Space between the bottom of one line and top of the next.
145    BetweenLines = 2,
146}
147
148impl fmt::Display for LineSpacingType {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        match self {
151            Self::Percentage => f.write_str("Percentage"),
152            Self::Fixed => f.write_str("Fixed"),
153            Self::BetweenLines => f.write_str("BetweenLines"),
154        }
155    }
156}
157
158impl std::str::FromStr for LineSpacingType {
159    type Err = FoundationError;
160
161    fn from_str(s: &str) -> Result<Self, Self::Err> {
162        match s {
163            "Percentage" | "percentage" => Ok(Self::Percentage),
164            "Fixed" | "fixed" => Ok(Self::Fixed),
165            "BetweenLines" | "betweenlines" | "between_lines" => Ok(Self::BetweenLines),
166            _ => Err(FoundationError::ParseError {
167                type_name: "LineSpacingType".to_string(),
168                value: s.to_string(),
169                valid_values: "Percentage, Fixed, BetweenLines".to_string(),
170            }),
171        }
172    }
173}
174
175impl TryFrom<u8> for LineSpacingType {
176    type Error = FoundationError;
177
178    fn try_from(value: u8) -> Result<Self, Self::Error> {
179        match value {
180            0 => Ok(Self::Percentage),
181            1 => Ok(Self::Fixed),
182            2 => Ok(Self::BetweenLines),
183            _ => Err(FoundationError::ParseError {
184                type_name: "LineSpacingType".to_string(),
185                value: value.to_string(),
186                valid_values: "0 (Percentage), 1 (Fixed), 2 (BetweenLines)".to_string(),
187            }),
188        }
189    }
190}
191
192impl schemars::JsonSchema for LineSpacingType {
193    fn schema_name() -> std::borrow::Cow<'static, str> {
194        std::borrow::Cow::Borrowed("LineSpacingType")
195    }
196
197    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
198        gen.subschema_for::<String>()
199    }
200}
201
202// ---------------------------------------------------------------------------
203// BreakType
204// ---------------------------------------------------------------------------
205
206/// Page/column break type before a paragraph.
207///
208/// # Examples
209///
210/// ```
211/// use hwpforge_foundation::BreakType;
212///
213/// assert_eq!(BreakType::default(), BreakType::None);
214/// ```
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
216#[non_exhaustive]
217#[repr(u8)]
218pub enum BreakType {
219    /// No break.
220    #[default]
221    None = 0,
222    /// Column break.
223    Column = 1,
224    /// Page break.
225    Page = 2,
226}
227
228impl fmt::Display for BreakType {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        match self {
231            Self::None => f.write_str("None"),
232            Self::Column => f.write_str("Column"),
233            Self::Page => f.write_str("Page"),
234        }
235    }
236}
237
238impl std::str::FromStr for BreakType {
239    type Err = FoundationError;
240
241    fn from_str(s: &str) -> Result<Self, Self::Err> {
242        match s {
243            "None" | "none" => Ok(Self::None),
244            "Column" | "column" => Ok(Self::Column),
245            "Page" | "page" => Ok(Self::Page),
246            _ => Err(FoundationError::ParseError {
247                type_name: "BreakType".to_string(),
248                value: s.to_string(),
249                valid_values: "None, Column, Page".to_string(),
250            }),
251        }
252    }
253}
254
255impl TryFrom<u8> for BreakType {
256    type Error = FoundationError;
257
258    fn try_from(value: u8) -> Result<Self, Self::Error> {
259        match value {
260            0 => Ok(Self::None),
261            1 => Ok(Self::Column),
262            2 => Ok(Self::Page),
263            _ => Err(FoundationError::ParseError {
264                type_name: "BreakType".to_string(),
265                value: value.to_string(),
266                valid_values: "0 (None), 1 (Column), 2 (Page)".to_string(),
267            }),
268        }
269    }
270}
271
272impl schemars::JsonSchema for BreakType {
273    fn schema_name() -> std::borrow::Cow<'static, str> {
274        std::borrow::Cow::Borrowed("BreakType")
275    }
276
277    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
278        gen.subschema_for::<String>()
279    }
280}
281
282// ---------------------------------------------------------------------------
283// Language
284// ---------------------------------------------------------------------------
285
286/// HWP5 language slots for font assignment.
287///
288/// Each character shape stores a font per language slot.
289/// The discriminant values match the HWP5 specification exactly.
290///
291/// # Examples
292///
293/// ```
294/// use hwpforge_foundation::Language;
295///
296/// assert_eq!(Language::COUNT, 7);
297/// assert_eq!(Language::Korean as u8, 0);
298/// ```
299#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
300#[non_exhaustive]
301#[repr(u8)]
302pub enum Language {
303    /// Korean (slot 0).
304    #[default]
305    Korean = 0,
306    /// English (slot 1).
307    English = 1,
308    /// Chinese characters / Hanja (slot 2).
309    Hanja = 2,
310    /// Japanese (slot 3).
311    Japanese = 3,
312    /// Other languages (slot 4).
313    Other = 4,
314    /// Symbol characters (slot 5).
315    Symbol = 5,
316    /// User-defined (slot 6).
317    User = 6,
318}
319
320impl Language {
321    /// Total number of language slots (7), matching the HWP5 spec.
322    pub const COUNT: usize = 7;
323
324    /// All language variants in slot order.
325    pub const ALL: [Self; 7] = [
326        Self::Korean,
327        Self::English,
328        Self::Hanja,
329        Self::Japanese,
330        Self::Other,
331        Self::Symbol,
332        Self::User,
333    ];
334}
335
336impl fmt::Display for Language {
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        match self {
339            Self::Korean => f.write_str("Korean"),
340            Self::English => f.write_str("English"),
341            Self::Hanja => f.write_str("Hanja"),
342            Self::Japanese => f.write_str("Japanese"),
343            Self::Other => f.write_str("Other"),
344            Self::Symbol => f.write_str("Symbol"),
345            Self::User => f.write_str("User"),
346        }
347    }
348}
349
350impl std::str::FromStr for Language {
351    type Err = FoundationError;
352
353    fn from_str(s: &str) -> Result<Self, Self::Err> {
354        match s {
355            "Korean" | "korean" => Ok(Self::Korean),
356            "English" | "english" => Ok(Self::English),
357            "Hanja" | "hanja" => Ok(Self::Hanja),
358            "Japanese" | "japanese" => Ok(Self::Japanese),
359            "Other" | "other" => Ok(Self::Other),
360            "Symbol" | "symbol" => Ok(Self::Symbol),
361            "User" | "user" => Ok(Self::User),
362            _ => Err(FoundationError::ParseError {
363                type_name: "Language".to_string(),
364                value: s.to_string(),
365                valid_values: "Korean, English, Hanja, Japanese, Other, Symbol, User".to_string(),
366            }),
367        }
368    }
369}
370
371impl TryFrom<u8> for Language {
372    type Error = FoundationError;
373
374    fn try_from(value: u8) -> Result<Self, Self::Error> {
375        match value {
376            0 => Ok(Self::Korean),
377            1 => Ok(Self::English),
378            2 => Ok(Self::Hanja),
379            3 => Ok(Self::Japanese),
380            4 => Ok(Self::Other),
381            5 => Ok(Self::Symbol),
382            6 => Ok(Self::User),
383            _ => Err(FoundationError::ParseError {
384                type_name: "Language".to_string(),
385                value: value.to_string(),
386                valid_values: "0-6 (Korean, English, Hanja, Japanese, Other, Symbol, User)"
387                    .to_string(),
388            }),
389        }
390    }
391}
392
393impl schemars::JsonSchema for Language {
394    fn schema_name() -> std::borrow::Cow<'static, str> {
395        std::borrow::Cow::Borrowed("Language")
396    }
397
398    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
399        gen.subschema_for::<String>()
400    }
401}
402
403// ---------------------------------------------------------------------------
404// UnderlineType
405// ---------------------------------------------------------------------------
406
407/// Underline decoration type.
408///
409/// # Examples
410///
411/// ```
412/// use hwpforge_foundation::UnderlineType;
413///
414/// assert_eq!(UnderlineType::default(), UnderlineType::None);
415/// ```
416#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
417#[non_exhaustive]
418#[repr(u8)]
419pub enum UnderlineType {
420    /// No underline (default).
421    #[default]
422    None = 0,
423    /// Single straight line below text.
424    Bottom = 1,
425    /// Single line centered on text.
426    Center = 2,
427    /// Single line above text.
428    Top = 3,
429}
430
431impl fmt::Display for UnderlineType {
432    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433        match self {
434            Self::None => f.write_str("None"),
435            Self::Bottom => f.write_str("Bottom"),
436            Self::Center => f.write_str("Center"),
437            Self::Top => f.write_str("Top"),
438        }
439    }
440}
441
442impl std::str::FromStr for UnderlineType {
443    type Err = FoundationError;
444
445    fn from_str(s: &str) -> Result<Self, Self::Err> {
446        match s {
447            "None" | "none" => Ok(Self::None),
448            "Bottom" | "bottom" => Ok(Self::Bottom),
449            "Center" | "center" => Ok(Self::Center),
450            "Top" | "top" => Ok(Self::Top),
451            _ => Err(FoundationError::ParseError {
452                type_name: "UnderlineType".to_string(),
453                value: s.to_string(),
454                valid_values: "None, Bottom, Center, Top".to_string(),
455            }),
456        }
457    }
458}
459
460impl TryFrom<u8> for UnderlineType {
461    type Error = FoundationError;
462
463    fn try_from(value: u8) -> Result<Self, Self::Error> {
464        match value {
465            0 => Ok(Self::None),
466            1 => Ok(Self::Bottom),
467            2 => Ok(Self::Center),
468            3 => Ok(Self::Top),
469            _ => Err(FoundationError::ParseError {
470                type_name: "UnderlineType".to_string(),
471                value: value.to_string(),
472                valid_values: "0 (None), 1 (Bottom), 2 (Center), 3 (Top)".to_string(),
473            }),
474        }
475    }
476}
477
478impl schemars::JsonSchema for UnderlineType {
479    fn schema_name() -> std::borrow::Cow<'static, str> {
480        std::borrow::Cow::Borrowed("UnderlineType")
481    }
482
483    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
484        gen.subschema_for::<String>()
485    }
486}
487
488// ---------------------------------------------------------------------------
489// StrikeoutShape
490// ---------------------------------------------------------------------------
491
492/// Strikeout line shape.
493///
494/// # Examples
495///
496/// ```
497/// use hwpforge_foundation::StrikeoutShape;
498///
499/// assert_eq!(StrikeoutShape::default(), StrikeoutShape::None);
500/// ```
501#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
502#[non_exhaustive]
503#[repr(u8)]
504pub enum StrikeoutShape {
505    /// No strikeout (default).
506    #[default]
507    None = 0,
508    /// Continuous straight line.
509    Continuous = 1,
510    /// Dashed line.
511    Dash = 2,
512    /// Dotted line.
513    Dot = 3,
514    /// Dash-dot pattern.
515    DashDot = 4,
516    /// Dash-dot-dot pattern.
517    DashDotDot = 5,
518}
519
520impl fmt::Display for StrikeoutShape {
521    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522        match self {
523            Self::None => f.write_str("None"),
524            Self::Continuous => f.write_str("Continuous"),
525            Self::Dash => f.write_str("Dash"),
526            Self::Dot => f.write_str("Dot"),
527            Self::DashDot => f.write_str("DashDot"),
528            Self::DashDotDot => f.write_str("DashDotDot"),
529        }
530    }
531}
532
533impl std::str::FromStr for StrikeoutShape {
534    type Err = FoundationError;
535
536    fn from_str(s: &str) -> Result<Self, Self::Err> {
537        match s {
538            "None" | "none" => Ok(Self::None),
539            "Continuous" | "continuous" => Ok(Self::Continuous),
540            "Dash" | "dash" => Ok(Self::Dash),
541            "Dot" | "dot" => Ok(Self::Dot),
542            "DashDot" | "dashdot" | "dash_dot" => Ok(Self::DashDot),
543            "DashDotDot" | "dashdotdot" | "dash_dot_dot" => Ok(Self::DashDotDot),
544            _ => Err(FoundationError::ParseError {
545                type_name: "StrikeoutShape".to_string(),
546                value: s.to_string(),
547                valid_values: "None, Continuous, Dash, Dot, DashDot, DashDotDot".to_string(),
548            }),
549        }
550    }
551}
552
553impl TryFrom<u8> for StrikeoutShape {
554    type Error = FoundationError;
555
556    fn try_from(value: u8) -> Result<Self, Self::Error> {
557        match value {
558            0 => Ok(Self::None),
559            1 => Ok(Self::Continuous),
560            2 => Ok(Self::Dash),
561            3 => Ok(Self::Dot),
562            4 => Ok(Self::DashDot),
563            5 => Ok(Self::DashDotDot),
564            _ => Err(FoundationError::ParseError {
565                type_name: "StrikeoutShape".to_string(),
566                value: value.to_string(),
567                valid_values: "0-5 (None, Continuous, Dash, Dot, DashDot, DashDotDot)".to_string(),
568            }),
569        }
570    }
571}
572
573impl schemars::JsonSchema for StrikeoutShape {
574    fn schema_name() -> std::borrow::Cow<'static, str> {
575        std::borrow::Cow::Borrowed("StrikeoutShape")
576    }
577
578    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
579        gen.subschema_for::<String>()
580    }
581}
582
583// ---------------------------------------------------------------------------
584// OutlineType
585// ---------------------------------------------------------------------------
586
587/// Text outline type (1pt border around glyphs).
588///
589/// # Examples
590///
591/// ```
592/// use hwpforge_foundation::OutlineType;
593///
594/// assert_eq!(OutlineType::default(), OutlineType::None);
595/// ```
596#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
597#[non_exhaustive]
598#[repr(u8)]
599pub enum OutlineType {
600    /// No outline (default).
601    #[default]
602    None = 0,
603    /// Solid 1pt outline.
604    Solid = 1,
605}
606
607impl fmt::Display for OutlineType {
608    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
609        match self {
610            Self::None => f.write_str("None"),
611            Self::Solid => f.write_str("Solid"),
612        }
613    }
614}
615
616impl std::str::FromStr for OutlineType {
617    type Err = FoundationError;
618
619    fn from_str(s: &str) -> Result<Self, Self::Err> {
620        match s {
621            "None" | "none" => Ok(Self::None),
622            "Solid" | "solid" => Ok(Self::Solid),
623            _ => Err(FoundationError::ParseError {
624                type_name: "OutlineType".to_string(),
625                value: s.to_string(),
626                valid_values: "None, Solid".to_string(),
627            }),
628        }
629    }
630}
631
632impl TryFrom<u8> for OutlineType {
633    type Error = FoundationError;
634
635    fn try_from(value: u8) -> Result<Self, Self::Error> {
636        match value {
637            0 => Ok(Self::None),
638            1 => Ok(Self::Solid),
639            _ => Err(FoundationError::ParseError {
640                type_name: "OutlineType".to_string(),
641                value: value.to_string(),
642                valid_values: "0 (None), 1 (Solid)".to_string(),
643            }),
644        }
645    }
646}
647
648impl schemars::JsonSchema for OutlineType {
649    fn schema_name() -> std::borrow::Cow<'static, str> {
650        std::borrow::Cow::Borrowed("OutlineType")
651    }
652
653    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
654        gen.subschema_for::<String>()
655    }
656}
657
658// ---------------------------------------------------------------------------
659// ShadowType
660// ---------------------------------------------------------------------------
661
662/// Text shadow type.
663///
664/// # Examples
665///
666/// ```
667/// use hwpforge_foundation::ShadowType;
668///
669/// assert_eq!(ShadowType::default(), ShadowType::None);
670/// ```
671#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
672#[non_exhaustive]
673#[repr(u8)]
674pub enum ShadowType {
675    /// No shadow (default).
676    #[default]
677    None = 0,
678    /// Drop shadow.
679    Drop = 1,
680}
681
682impl fmt::Display for ShadowType {
683    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
684        match self {
685            Self::None => f.write_str("None"),
686            Self::Drop => f.write_str("Drop"),
687        }
688    }
689}
690
691impl std::str::FromStr for ShadowType {
692    type Err = FoundationError;
693
694    fn from_str(s: &str) -> Result<Self, Self::Err> {
695        match s {
696            "None" | "none" => Ok(Self::None),
697            "Drop" | "drop" => Ok(Self::Drop),
698            _ => Err(FoundationError::ParseError {
699                type_name: "ShadowType".to_string(),
700                value: s.to_string(),
701                valid_values: "None, Drop".to_string(),
702            }),
703        }
704    }
705}
706
707impl TryFrom<u8> for ShadowType {
708    type Error = FoundationError;
709
710    fn try_from(value: u8) -> Result<Self, Self::Error> {
711        match value {
712            0 => Ok(Self::None),
713            1 => Ok(Self::Drop),
714            _ => Err(FoundationError::ParseError {
715                type_name: "ShadowType".to_string(),
716                value: value.to_string(),
717                valid_values: "0 (None), 1 (Drop)".to_string(),
718            }),
719        }
720    }
721}
722
723impl schemars::JsonSchema for ShadowType {
724    fn schema_name() -> std::borrow::Cow<'static, str> {
725        std::borrow::Cow::Borrowed("ShadowType")
726    }
727
728    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
729        gen.subschema_for::<String>()
730    }
731}
732
733// ---------------------------------------------------------------------------
734// EmbossType
735// ---------------------------------------------------------------------------
736
737/// Text embossing (raised appearance).
738///
739/// # Examples
740///
741/// ```
742/// use hwpforge_foundation::EmbossType;
743///
744/// assert_eq!(EmbossType::default(), EmbossType::None);
745/// ```
746#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
747#[non_exhaustive]
748#[repr(u8)]
749pub enum EmbossType {
750    /// No emboss (default).
751    #[default]
752    None = 0,
753    /// Raised emboss effect.
754    Emboss = 1,
755}
756
757impl fmt::Display for EmbossType {
758    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
759        match self {
760            Self::None => f.write_str("None"),
761            Self::Emboss => f.write_str("Emboss"),
762        }
763    }
764}
765
766impl std::str::FromStr for EmbossType {
767    type Err = FoundationError;
768
769    fn from_str(s: &str) -> Result<Self, Self::Err> {
770        match s {
771            "None" | "none" => Ok(Self::None),
772            "Emboss" | "emboss" => Ok(Self::Emboss),
773            _ => Err(FoundationError::ParseError {
774                type_name: "EmbossType".to_string(),
775                value: s.to_string(),
776                valid_values: "None, Emboss".to_string(),
777            }),
778        }
779    }
780}
781
782impl TryFrom<u8> for EmbossType {
783    type Error = FoundationError;
784
785    fn try_from(value: u8) -> Result<Self, Self::Error> {
786        match value {
787            0 => Ok(Self::None),
788            1 => Ok(Self::Emboss),
789            _ => Err(FoundationError::ParseError {
790                type_name: "EmbossType".to_string(),
791                value: value.to_string(),
792                valid_values: "0 (None), 1 (Emboss)".to_string(),
793            }),
794        }
795    }
796}
797
798impl schemars::JsonSchema for EmbossType {
799    fn schema_name() -> std::borrow::Cow<'static, str> {
800        std::borrow::Cow::Borrowed("EmbossType")
801    }
802
803    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
804        gen.subschema_for::<String>()
805    }
806}
807
808// ---------------------------------------------------------------------------
809// EngraveType
810// ---------------------------------------------------------------------------
811
812/// Text engraving (sunken appearance).
813///
814/// # Examples
815///
816/// ```
817/// use hwpforge_foundation::EngraveType;
818///
819/// assert_eq!(EngraveType::default(), EngraveType::None);
820/// ```
821#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
822#[non_exhaustive]
823#[repr(u8)]
824pub enum EngraveType {
825    /// No engrave (default).
826    #[default]
827    None = 0,
828    /// Sunken engrave effect.
829    Engrave = 1,
830}
831
832impl fmt::Display for EngraveType {
833    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
834        match self {
835            Self::None => f.write_str("None"),
836            Self::Engrave => f.write_str("Engrave"),
837        }
838    }
839}
840
841impl std::str::FromStr for EngraveType {
842    type Err = FoundationError;
843
844    fn from_str(s: &str) -> Result<Self, Self::Err> {
845        match s {
846            "None" | "none" => Ok(Self::None),
847            "Engrave" | "engrave" => Ok(Self::Engrave),
848            _ => Err(FoundationError::ParseError {
849                type_name: "EngraveType".to_string(),
850                value: s.to_string(),
851                valid_values: "None, Engrave".to_string(),
852            }),
853        }
854    }
855}
856
857impl TryFrom<u8> for EngraveType {
858    type Error = FoundationError;
859
860    fn try_from(value: u8) -> Result<Self, Self::Error> {
861        match value {
862            0 => Ok(Self::None),
863            1 => Ok(Self::Engrave),
864            _ => Err(FoundationError::ParseError {
865                type_name: "EngraveType".to_string(),
866                value: value.to_string(),
867                valid_values: "0 (None), 1 (Engrave)".to_string(),
868            }),
869        }
870    }
871}
872
873impl schemars::JsonSchema for EngraveType {
874    fn schema_name() -> std::borrow::Cow<'static, str> {
875        std::borrow::Cow::Borrowed("EngraveType")
876    }
877
878    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
879        gen.subschema_for::<String>()
880    }
881}
882
883// ---------------------------------------------------------------------------
884// VerticalPosition
885// ---------------------------------------------------------------------------
886
887/// Superscript/subscript position type.
888///
889/// # Examples
890///
891/// ```
892/// use hwpforge_foundation::VerticalPosition;
893///
894/// assert_eq!(VerticalPosition::default(), VerticalPosition::Normal);
895/// ```
896#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
897#[non_exhaustive]
898#[repr(u8)]
899pub enum VerticalPosition {
900    /// Normal baseline (default).
901    #[default]
902    Normal = 0,
903    /// Superscript.
904    Superscript = 1,
905    /// Subscript.
906    Subscript = 2,
907}
908
909impl fmt::Display for VerticalPosition {
910    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
911        match self {
912            Self::Normal => f.write_str("Normal"),
913            Self::Superscript => f.write_str("Superscript"),
914            Self::Subscript => f.write_str("Subscript"),
915        }
916    }
917}
918
919impl std::str::FromStr for VerticalPosition {
920    type Err = FoundationError;
921
922    fn from_str(s: &str) -> Result<Self, Self::Err> {
923        match s {
924            "Normal" | "normal" => Ok(Self::Normal),
925            "Superscript" | "superscript" | "super" => Ok(Self::Superscript),
926            "Subscript" | "subscript" | "sub" => Ok(Self::Subscript),
927            _ => Err(FoundationError::ParseError {
928                type_name: "VerticalPosition".to_string(),
929                value: s.to_string(),
930                valid_values: "Normal, Superscript, Subscript".to_string(),
931            }),
932        }
933    }
934}
935
936impl TryFrom<u8> for VerticalPosition {
937    type Error = FoundationError;
938
939    fn try_from(value: u8) -> Result<Self, Self::Error> {
940        match value {
941            0 => Ok(Self::Normal),
942            1 => Ok(Self::Superscript),
943            2 => Ok(Self::Subscript),
944            _ => Err(FoundationError::ParseError {
945                type_name: "VerticalPosition".to_string(),
946                value: value.to_string(),
947                valid_values: "0 (Normal), 1 (Superscript), 2 (Subscript)".to_string(),
948            }),
949        }
950    }
951}
952
953impl schemars::JsonSchema for VerticalPosition {
954    fn schema_name() -> std::borrow::Cow<'static, str> {
955        std::borrow::Cow::Borrowed("VerticalPosition")
956    }
957
958    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
959        gen.subschema_for::<String>()
960    }
961}
962
963// ---------------------------------------------------------------------------
964// BorderLineType
965// ---------------------------------------------------------------------------
966
967/// Border line type.
968///
969/// # Examples
970///
971/// ```
972/// use hwpforge_foundation::BorderLineType;
973///
974/// assert_eq!(BorderLineType::default(), BorderLineType::None);
975/// ```
976#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
977#[non_exhaustive]
978#[repr(u8)]
979pub enum BorderLineType {
980    /// No border.
981    #[default]
982    None = 0,
983    /// Solid line.
984    Solid = 1,
985    /// Dashed line.
986    Dash = 2,
987    /// Dotted line.
988    Dot = 3,
989    /// Dash-dot pattern.
990    DashDot = 4,
991    /// Dash-dot-dot pattern.
992    DashDotDot = 5,
993    /// Long dash pattern.
994    LongDash = 6,
995    /// Triple dot pattern.
996    TripleDot = 7,
997    /// Double line.
998    Double = 8,
999    /// Thin-thick double.
1000    DoubleSlim = 9,
1001    /// Thick-thin double.
1002    ThickBetweenSlim = 10,
1003}
1004
1005impl fmt::Display for BorderLineType {
1006    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1007        match self {
1008            Self::None => f.write_str("None"),
1009            Self::Solid => f.write_str("Solid"),
1010            Self::Dash => f.write_str("Dash"),
1011            Self::Dot => f.write_str("Dot"),
1012            Self::DashDot => f.write_str("DashDot"),
1013            Self::DashDotDot => f.write_str("DashDotDot"),
1014            Self::LongDash => f.write_str("LongDash"),
1015            Self::TripleDot => f.write_str("TripleDot"),
1016            Self::Double => f.write_str("Double"),
1017            Self::DoubleSlim => f.write_str("DoubleSlim"),
1018            Self::ThickBetweenSlim => f.write_str("ThickBetweenSlim"),
1019        }
1020    }
1021}
1022
1023impl std::str::FromStr for BorderLineType {
1024    type Err = FoundationError;
1025
1026    fn from_str(s: &str) -> Result<Self, Self::Err> {
1027        match s {
1028            "None" | "none" => Ok(Self::None),
1029            "Solid" | "solid" => Ok(Self::Solid),
1030            "Dash" | "dash" => Ok(Self::Dash),
1031            "Dot" | "dot" => Ok(Self::Dot),
1032            "DashDot" | "dashdot" | "dash_dot" => Ok(Self::DashDot),
1033            "DashDotDot" | "dashdotdot" | "dash_dot_dot" => Ok(Self::DashDotDot),
1034            "LongDash" | "longdash" | "long_dash" => Ok(Self::LongDash),
1035            "TripleDot" | "tripledot" | "triple_dot" => Ok(Self::TripleDot),
1036            "Double" | "double" => Ok(Self::Double),
1037            "DoubleSlim" | "doubleslim" | "double_slim" => Ok(Self::DoubleSlim),
1038            "ThickBetweenSlim" | "thickbetweenslim" | "thick_between_slim" => {
1039                Ok(Self::ThickBetweenSlim)
1040            }
1041            _ => Err(FoundationError::ParseError {
1042                type_name: "BorderLineType".to_string(),
1043                value: s.to_string(),
1044                valid_values: "None, Solid, Dash, Dot, DashDot, DashDotDot, LongDash, TripleDot, Double, DoubleSlim, ThickBetweenSlim".to_string(),
1045            }),
1046        }
1047    }
1048}
1049
1050impl TryFrom<u8> for BorderLineType {
1051    type Error = FoundationError;
1052
1053    fn try_from(value: u8) -> Result<Self, Self::Error> {
1054        match value {
1055            0 => Ok(Self::None),
1056            1 => Ok(Self::Solid),
1057            2 => Ok(Self::Dash),
1058            3 => Ok(Self::Dot),
1059            4 => Ok(Self::DashDot),
1060            5 => Ok(Self::DashDotDot),
1061            6 => Ok(Self::LongDash),
1062            7 => Ok(Self::TripleDot),
1063            8 => Ok(Self::Double),
1064            9 => Ok(Self::DoubleSlim),
1065            10 => Ok(Self::ThickBetweenSlim),
1066            _ => Err(FoundationError::ParseError {
1067                type_name: "BorderLineType".to_string(),
1068                value: value.to_string(),
1069                valid_values: "0-10 (None, Solid, Dash, Dot, DashDot, DashDotDot, LongDash, TripleDot, Double, DoubleSlim, ThickBetweenSlim)".to_string(),
1070            }),
1071        }
1072    }
1073}
1074
1075impl schemars::JsonSchema for BorderLineType {
1076    fn schema_name() -> std::borrow::Cow<'static, str> {
1077        std::borrow::Cow::Borrowed("BorderLineType")
1078    }
1079
1080    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1081        gen.subschema_for::<String>()
1082    }
1083}
1084
1085// ---------------------------------------------------------------------------
1086// FillBrushType
1087// ---------------------------------------------------------------------------
1088
1089/// Fill brush type for backgrounds.
1090///
1091/// # Examples
1092///
1093/// ```
1094/// use hwpforge_foundation::FillBrushType;
1095///
1096/// assert_eq!(FillBrushType::default(), FillBrushType::None);
1097/// ```
1098#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1099#[non_exhaustive]
1100#[repr(u8)]
1101pub enum FillBrushType {
1102    /// No fill (transparent, default).
1103    #[default]
1104    None = 0,
1105    /// Solid color fill.
1106    Solid = 1,
1107    /// Gradient fill (linear or radial).
1108    Gradient = 2,
1109    /// Pattern fill (hatch, dots, etc.).
1110    Pattern = 3,
1111}
1112
1113impl fmt::Display for FillBrushType {
1114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1115        match self {
1116            Self::None => f.write_str("None"),
1117            Self::Solid => f.write_str("Solid"),
1118            Self::Gradient => f.write_str("Gradient"),
1119            Self::Pattern => f.write_str("Pattern"),
1120        }
1121    }
1122}
1123
1124impl std::str::FromStr for FillBrushType {
1125    type Err = FoundationError;
1126
1127    fn from_str(s: &str) -> Result<Self, Self::Err> {
1128        match s {
1129            "None" | "none" => Ok(Self::None),
1130            "Solid" | "solid" => Ok(Self::Solid),
1131            "Gradient" | "gradient" => Ok(Self::Gradient),
1132            "Pattern" | "pattern" => Ok(Self::Pattern),
1133            _ => Err(FoundationError::ParseError {
1134                type_name: "FillBrushType".to_string(),
1135                value: s.to_string(),
1136                valid_values: "None, Solid, Gradient, Pattern".to_string(),
1137            }),
1138        }
1139    }
1140}
1141
1142impl TryFrom<u8> for FillBrushType {
1143    type Error = FoundationError;
1144
1145    fn try_from(value: u8) -> Result<Self, Self::Error> {
1146        match value {
1147            0 => Ok(Self::None),
1148            1 => Ok(Self::Solid),
1149            2 => Ok(Self::Gradient),
1150            3 => Ok(Self::Pattern),
1151            _ => Err(FoundationError::ParseError {
1152                type_name: "FillBrushType".to_string(),
1153                value: value.to_string(),
1154                valid_values: "0 (None), 1 (Solid), 2 (Gradient), 3 (Pattern)".to_string(),
1155            }),
1156        }
1157    }
1158}
1159
1160impl schemars::JsonSchema for FillBrushType {
1161    fn schema_name() -> std::borrow::Cow<'static, str> {
1162        std::borrow::Cow::Borrowed("FillBrushType")
1163    }
1164
1165    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1166        gen.subschema_for::<String>()
1167    }
1168}
1169
1170// ---------------------------------------------------------------------------
1171// ApplyPageType
1172// ---------------------------------------------------------------------------
1173
1174/// Which pages a header/footer applies to.
1175///
1176/// # Examples
1177///
1178/// ```
1179/// use hwpforge_foundation::ApplyPageType;
1180///
1181/// assert_eq!(ApplyPageType::default(), ApplyPageType::Both);
1182/// ```
1183#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1184#[non_exhaustive]
1185#[repr(u8)]
1186pub enum ApplyPageType {
1187    /// Both even and odd pages (default).
1188    #[default]
1189    Both = 0,
1190    /// Even pages only.
1191    Even = 1,
1192    /// Odd pages only.
1193    Odd = 2,
1194}
1195
1196impl fmt::Display for ApplyPageType {
1197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1198        match self {
1199            Self::Both => f.write_str("Both"),
1200            Self::Even => f.write_str("Even"),
1201            Self::Odd => f.write_str("Odd"),
1202        }
1203    }
1204}
1205
1206impl std::str::FromStr for ApplyPageType {
1207    type Err = FoundationError;
1208
1209    fn from_str(s: &str) -> Result<Self, Self::Err> {
1210        match s {
1211            "Both" | "both" | "BOTH" => Ok(Self::Both),
1212            "Even" | "even" | "EVEN" => Ok(Self::Even),
1213            "Odd" | "odd" | "ODD" => Ok(Self::Odd),
1214            _ => Err(FoundationError::ParseError {
1215                type_name: "ApplyPageType".to_string(),
1216                value: s.to_string(),
1217                valid_values: "Both, Even, Odd".to_string(),
1218            }),
1219        }
1220    }
1221}
1222
1223impl TryFrom<u8> for ApplyPageType {
1224    type Error = FoundationError;
1225
1226    fn try_from(value: u8) -> Result<Self, Self::Error> {
1227        match value {
1228            0 => Ok(Self::Both),
1229            1 => Ok(Self::Even),
1230            2 => Ok(Self::Odd),
1231            _ => Err(FoundationError::ParseError {
1232                type_name: "ApplyPageType".to_string(),
1233                value: value.to_string(),
1234                valid_values: "0 (Both), 1 (Even), 2 (Odd)".to_string(),
1235            }),
1236        }
1237    }
1238}
1239
1240impl schemars::JsonSchema for ApplyPageType {
1241    fn schema_name() -> std::borrow::Cow<'static, str> {
1242        std::borrow::Cow::Borrowed("ApplyPageType")
1243    }
1244
1245    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1246        gen.subschema_for::<String>()
1247    }
1248}
1249
1250// ---------------------------------------------------------------------------
1251// NumberFormatType
1252// ---------------------------------------------------------------------------
1253
1254/// Number format for page numbering.
1255///
1256/// # Examples
1257///
1258/// ```
1259/// use hwpforge_foundation::NumberFormatType;
1260///
1261/// assert_eq!(NumberFormatType::default(), NumberFormatType::Digit);
1262/// ```
1263#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1264#[non_exhaustive]
1265#[repr(u8)]
1266pub enum NumberFormatType {
1267    /// Arabic digits: 1, 2, 3, ... (default).
1268    #[default]
1269    Digit = 0,
1270    /// Circled digits: ①, ②, ③, ...
1271    CircledDigit = 1,
1272    /// Roman capitals: I, II, III, ...
1273    RomanCapital = 2,
1274    /// Roman lowercase: i, ii, iii, ...
1275    RomanSmall = 3,
1276    /// Latin capitals: A, B, C, ...
1277    LatinCapital = 4,
1278    /// Latin lowercase: a, b, c, ...
1279    LatinSmall = 5,
1280    /// Hangul syllable: 가, 나, 다, ...
1281    HangulSyllable = 6,
1282    /// Hangul jamo: ㄱ, ㄴ, ㄷ, ...
1283    HangulJamo = 7,
1284    /// Hanja digits: 一, 二, 三, ...
1285    HanjaDigit = 8,
1286    /// Circled Hangul syllable: ㉮, ㉯, ㉰, ... (used for outline level 8).
1287    CircledHangulSyllable = 9,
1288    /// Circled Latin lowercase: ⓐ, ⓑ, ⓒ, ...
1289    CircledLatinSmall = 10,
1290}
1291
1292impl fmt::Display for NumberFormatType {
1293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1294        match self {
1295            Self::Digit => f.write_str("Digit"),
1296            Self::CircledDigit => f.write_str("CircledDigit"),
1297            Self::RomanCapital => f.write_str("RomanCapital"),
1298            Self::RomanSmall => f.write_str("RomanSmall"),
1299            Self::LatinCapital => f.write_str("LatinCapital"),
1300            Self::LatinSmall => f.write_str("LatinSmall"),
1301            Self::HangulSyllable => f.write_str("HangulSyllable"),
1302            Self::HangulJamo => f.write_str("HangulJamo"),
1303            Self::HanjaDigit => f.write_str("HanjaDigit"),
1304            Self::CircledHangulSyllable => f.write_str("CircledHangulSyllable"),
1305            Self::CircledLatinSmall => f.write_str("CircledLatinSmall"),
1306        }
1307    }
1308}
1309
1310impl std::str::FromStr for NumberFormatType {
1311    type Err = FoundationError;
1312
1313    fn from_str(s: &str) -> Result<Self, Self::Err> {
1314        match s {
1315            "Digit" | "digit" | "DIGIT" => Ok(Self::Digit),
1316            "CircledDigit" | "circleddigit" | "CIRCLED_DIGIT" => Ok(Self::CircledDigit),
1317            "RomanCapital" | "romancapital" | "ROMAN_CAPITAL" => Ok(Self::RomanCapital),
1318            "RomanSmall" | "romansmall" | "ROMAN_SMALL" => Ok(Self::RomanSmall),
1319            "LatinCapital" | "latincapital" | "LATIN_CAPITAL" => Ok(Self::LatinCapital),
1320            "LatinSmall" | "latinsmall" | "LATIN_SMALL" => Ok(Self::LatinSmall),
1321            "HangulSyllable" | "hangulsyllable" | "HANGUL_SYLLABLE" => Ok(Self::HangulSyllable),
1322            "HangulJamo" | "hanguljamo" | "HANGUL_JAMO" => Ok(Self::HangulJamo),
1323            "HanjaDigit" | "hanjadigit" | "HANJA_DIGIT" => Ok(Self::HanjaDigit),
1324            "CircledHangulSyllable" | "circledhangulsyllable" | "CIRCLED_HANGUL_SYLLABLE" => {
1325                Ok(Self::CircledHangulSyllable)
1326            }
1327            "CircledLatinSmall" | "circledlatinsmall" | "CIRCLED_LATIN_SMALL" => {
1328                Ok(Self::CircledLatinSmall)
1329            }
1330            _ => Err(FoundationError::ParseError {
1331                type_name: "NumberFormatType".to_string(),
1332                value: s.to_string(),
1333                valid_values: "Digit, CircledDigit, RomanCapital, RomanSmall, LatinCapital, LatinSmall, HangulSyllable, HangulJamo, HanjaDigit, CircledHangulSyllable, CircledLatinSmall".to_string(),
1334            }),
1335        }
1336    }
1337}
1338
1339impl TryFrom<u8> for NumberFormatType {
1340    type Error = FoundationError;
1341
1342    fn try_from(value: u8) -> Result<Self, Self::Error> {
1343        match value {
1344            0 => Ok(Self::Digit),
1345            1 => Ok(Self::CircledDigit),
1346            2 => Ok(Self::RomanCapital),
1347            3 => Ok(Self::RomanSmall),
1348            4 => Ok(Self::LatinCapital),
1349            5 => Ok(Self::LatinSmall),
1350            6 => Ok(Self::HangulSyllable),
1351            7 => Ok(Self::HangulJamo),
1352            8 => Ok(Self::HanjaDigit),
1353            9 => Ok(Self::CircledHangulSyllable),
1354            10 => Ok(Self::CircledLatinSmall),
1355            _ => Err(FoundationError::ParseError {
1356                type_name: "NumberFormatType".to_string(),
1357                value: value.to_string(),
1358                valid_values: "0-10 (Digit, CircledDigit, RomanCapital, RomanSmall, LatinCapital, LatinSmall, HangulSyllable, HangulJamo, HanjaDigit, CircledHangulSyllable, CircledLatinSmall)".to_string(),
1359            }),
1360        }
1361    }
1362}
1363
1364impl schemars::JsonSchema for NumberFormatType {
1365    fn schema_name() -> std::borrow::Cow<'static, str> {
1366        std::borrow::Cow::Borrowed("NumberFormatType")
1367    }
1368
1369    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1370        gen.subschema_for::<String>()
1371    }
1372}
1373
1374// ---------------------------------------------------------------------------
1375// PageNumberPosition
1376// ---------------------------------------------------------------------------
1377
1378/// Position of page numbers on the page.
1379///
1380/// # Examples
1381///
1382/// ```
1383/// use hwpforge_foundation::PageNumberPosition;
1384///
1385/// assert_eq!(PageNumberPosition::default(), PageNumberPosition::TopCenter);
1386/// ```
1387#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1388#[non_exhaustive]
1389#[repr(u8)]
1390pub enum PageNumberPosition {
1391    /// No page number.
1392    None = 0,
1393    /// Top left.
1394    TopLeft = 1,
1395    /// Top center (default).
1396    #[default]
1397    TopCenter = 2,
1398    /// Top right.
1399    TopRight = 3,
1400    /// Bottom left.
1401    BottomLeft = 4,
1402    /// Bottom center.
1403    BottomCenter = 5,
1404    /// Bottom right.
1405    BottomRight = 6,
1406    /// Outside top.
1407    OutsideTop = 7,
1408    /// Outside bottom.
1409    OutsideBottom = 8,
1410    /// Inside top.
1411    InsideTop = 9,
1412    /// Inside bottom.
1413    InsideBottom = 10,
1414}
1415
1416impl fmt::Display for PageNumberPosition {
1417    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1418        match self {
1419            Self::None => f.write_str("None"),
1420            Self::TopLeft => f.write_str("TopLeft"),
1421            Self::TopCenter => f.write_str("TopCenter"),
1422            Self::TopRight => f.write_str("TopRight"),
1423            Self::BottomLeft => f.write_str("BottomLeft"),
1424            Self::BottomCenter => f.write_str("BottomCenter"),
1425            Self::BottomRight => f.write_str("BottomRight"),
1426            Self::OutsideTop => f.write_str("OutsideTop"),
1427            Self::OutsideBottom => f.write_str("OutsideBottom"),
1428            Self::InsideTop => f.write_str("InsideTop"),
1429            Self::InsideBottom => f.write_str("InsideBottom"),
1430        }
1431    }
1432}
1433
1434impl std::str::FromStr for PageNumberPosition {
1435    type Err = FoundationError;
1436
1437    fn from_str(s: &str) -> Result<Self, Self::Err> {
1438        match s {
1439            "None" | "none" | "NONE" => Ok(Self::None),
1440            "TopLeft" | "topleft" | "TOP_LEFT" | "top-left" => Ok(Self::TopLeft),
1441            "TopCenter" | "topcenter" | "TOP_CENTER" | "top-center" => Ok(Self::TopCenter),
1442            "TopRight" | "topright" | "TOP_RIGHT" | "top-right" => Ok(Self::TopRight),
1443            "BottomLeft" | "bottomleft" | "BOTTOM_LEFT" | "bottom-left" => Ok(Self::BottomLeft),
1444            "BottomCenter" | "bottomcenter" | "BOTTOM_CENTER" | "bottom-center" => {
1445                Ok(Self::BottomCenter)
1446            }
1447            "BottomRight" | "bottomright" | "BOTTOM_RIGHT" | "bottom-right" => {
1448                Ok(Self::BottomRight)
1449            }
1450            "OutsideTop" | "outsidetop" | "OUTSIDE_TOP" | "outside-top" => Ok(Self::OutsideTop),
1451            "OutsideBottom" | "outsidebottom" | "OUTSIDE_BOTTOM" | "outside-bottom" => {
1452                Ok(Self::OutsideBottom)
1453            }
1454            "InsideTop" | "insidetop" | "INSIDE_TOP" | "inside-top" => Ok(Self::InsideTop),
1455            "InsideBottom" | "insidebottom" | "INSIDE_BOTTOM" | "inside-bottom" => {
1456                Ok(Self::InsideBottom)
1457            }
1458            _ => Err(FoundationError::ParseError {
1459                type_name: "PageNumberPosition".to_string(),
1460                value: s.to_string(),
1461                valid_values: "None, TopLeft, TopCenter, TopRight, BottomLeft, BottomCenter, BottomRight, OutsideTop, OutsideBottom, InsideTop, InsideBottom".to_string(),
1462            }),
1463        }
1464    }
1465}
1466
1467impl TryFrom<u8> for PageNumberPosition {
1468    type Error = FoundationError;
1469
1470    fn try_from(value: u8) -> Result<Self, Self::Error> {
1471        match value {
1472            0 => Ok(Self::None),
1473            1 => Ok(Self::TopLeft),
1474            2 => Ok(Self::TopCenter),
1475            3 => Ok(Self::TopRight),
1476            4 => Ok(Self::BottomLeft),
1477            5 => Ok(Self::BottomCenter),
1478            6 => Ok(Self::BottomRight),
1479            7 => Ok(Self::OutsideTop),
1480            8 => Ok(Self::OutsideBottom),
1481            9 => Ok(Self::InsideTop),
1482            10 => Ok(Self::InsideBottom),
1483            _ => Err(FoundationError::ParseError {
1484                type_name: "PageNumberPosition".to_string(),
1485                value: value.to_string(),
1486                valid_values: "0-10 (None, TopLeft, TopCenter, TopRight, BottomLeft, BottomCenter, BottomRight, OutsideTop, OutsideBottom, InsideTop, InsideBottom)".to_string(),
1487            }),
1488        }
1489    }
1490}
1491
1492impl schemars::JsonSchema for PageNumberPosition {
1493    fn schema_name() -> std::borrow::Cow<'static, str> {
1494        std::borrow::Cow::Borrowed("PageNumberPosition")
1495    }
1496
1497    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1498        gen.subschema_for::<String>()
1499    }
1500}
1501
1502// ---------------------------------------------------------------------------
1503// WordBreakType
1504// ---------------------------------------------------------------------------
1505
1506/// Word-breaking behavior for paragraph text justification.
1507///
1508/// Controls how 한글 distributes extra space in justified text.
1509/// `KeepWord` preserves word boundaries (natural spacing),
1510/// `BreakWord` allows breaking at any character (stretched spacing).
1511///
1512/// # Examples
1513///
1514/// ```
1515/// use hwpforge_foundation::WordBreakType;
1516///
1517/// assert_eq!(WordBreakType::default(), WordBreakType::KeepWord);
1518/// assert_eq!(WordBreakType::KeepWord.to_string(), "KEEP_WORD");
1519/// ```
1520#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1521#[non_exhaustive]
1522#[repr(u8)]
1523pub enum WordBreakType {
1524    /// Keep words intact — distribute space between words only (한글 default).
1525    #[default]
1526    KeepWord = 0,
1527    /// Allow breaking at any character — distribute space between all characters.
1528    BreakWord = 1,
1529}
1530
1531impl fmt::Display for WordBreakType {
1532    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1533        match self {
1534            Self::KeepWord => f.write_str("KEEP_WORD"),
1535            Self::BreakWord => f.write_str("BREAK_WORD"),
1536        }
1537    }
1538}
1539
1540impl std::str::FromStr for WordBreakType {
1541    type Err = FoundationError;
1542
1543    fn from_str(s: &str) -> Result<Self, Self::Err> {
1544        match s {
1545            "KEEP_WORD" | "KeepWord" | "keep_word" => Ok(Self::KeepWord),
1546            "BREAK_WORD" | "BreakWord" | "break_word" => Ok(Self::BreakWord),
1547            _ => Err(FoundationError::ParseError {
1548                type_name: "WordBreakType".to_string(),
1549                value: s.to_string(),
1550                valid_values: "KEEP_WORD, BREAK_WORD".to_string(),
1551            }),
1552        }
1553    }
1554}
1555
1556impl TryFrom<u8> for WordBreakType {
1557    type Error = FoundationError;
1558
1559    fn try_from(value: u8) -> Result<Self, Self::Error> {
1560        match value {
1561            0 => Ok(Self::KeepWord),
1562            1 => Ok(Self::BreakWord),
1563            _ => Err(FoundationError::ParseError {
1564                type_name: "WordBreakType".to_string(),
1565                value: value.to_string(),
1566                valid_values: "0 (KeepWord), 1 (BreakWord)".to_string(),
1567            }),
1568        }
1569    }
1570}
1571
1572impl schemars::JsonSchema for WordBreakType {
1573    fn schema_name() -> std::borrow::Cow<'static, str> {
1574        std::borrow::Cow::Borrowed("WordBreakType")
1575    }
1576
1577    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1578        gen.subschema_for::<String>()
1579    }
1580}
1581
1582// ---------------------------------------------------------------------------
1583// EmphasisType
1584// ---------------------------------------------------------------------------
1585
1586/// Character emphasis mark (symMark attribute in HWPX).
1587///
1588/// Controls the emphasis symbol displayed above or below characters.
1589/// Maps to HWPX `symMark` attribute values.
1590///
1591/// # Examples
1592///
1593/// ```
1594/// use hwpforge_foundation::EmphasisType;
1595///
1596/// assert_eq!(EmphasisType::default(), EmphasisType::None);
1597/// ```
1598#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1599#[non_exhaustive]
1600#[repr(u8)]
1601pub enum EmphasisType {
1602    /// No emphasis mark (default).
1603    #[default]
1604    None = 0,
1605    /// Dot above character.
1606    DotAbove = 1,
1607    /// Ring above character.
1608    RingAbove = 2,
1609    /// Tilde above character.
1610    Tilde = 3,
1611    /// Caron (hacek) above character.
1612    Caron = 4,
1613    /// Side dot.
1614    Side = 5,
1615    /// Colon mark.
1616    Colon = 6,
1617    /// Grave accent.
1618    GraveAccent = 7,
1619    /// Acute accent.
1620    AcuteAccent = 8,
1621    /// Circumflex accent.
1622    Circumflex = 9,
1623    /// Macron (overline).
1624    Macron = 10,
1625    /// Hook above.
1626    HookAbove = 11,
1627    /// Dot below character.
1628    DotBelow = 12,
1629}
1630
1631impl fmt::Display for EmphasisType {
1632    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1633        match self {
1634            Self::None => f.write_str("None"),
1635            Self::DotAbove => f.write_str("DotAbove"),
1636            Self::RingAbove => f.write_str("RingAbove"),
1637            Self::Tilde => f.write_str("Tilde"),
1638            Self::Caron => f.write_str("Caron"),
1639            Self::Side => f.write_str("Side"),
1640            Self::Colon => f.write_str("Colon"),
1641            Self::GraveAccent => f.write_str("GraveAccent"),
1642            Self::AcuteAccent => f.write_str("AcuteAccent"),
1643            Self::Circumflex => f.write_str("Circumflex"),
1644            Self::Macron => f.write_str("Macron"),
1645            Self::HookAbove => f.write_str("HookAbove"),
1646            Self::DotBelow => f.write_str("DotBelow"),
1647        }
1648    }
1649}
1650
1651impl std::str::FromStr for EmphasisType {
1652    type Err = FoundationError;
1653
1654    fn from_str(s: &str) -> Result<Self, Self::Err> {
1655        match s {
1656            "NONE" | "None" | "none" => Ok(Self::None),
1657            "DOT_ABOVE" | "DotAbove" | "dot_above" => Ok(Self::DotAbove),
1658            "RING_ABOVE" | "RingAbove" | "ring_above" => Ok(Self::RingAbove),
1659            "TILDE" | "Tilde" | "tilde" => Ok(Self::Tilde),
1660            "CARON" | "Caron" | "caron" => Ok(Self::Caron),
1661            "SIDE" | "Side" | "side" => Ok(Self::Side),
1662            "COLON" | "Colon" | "colon" => Ok(Self::Colon),
1663            "GRAVE_ACCENT" | "GraveAccent" | "grave_accent" => Ok(Self::GraveAccent),
1664            "ACUTE_ACCENT" | "AcuteAccent" | "acute_accent" => Ok(Self::AcuteAccent),
1665            "CIRCUMFLEX" | "Circumflex" | "circumflex" => Ok(Self::Circumflex),
1666            "MACRON" | "Macron" | "macron" => Ok(Self::Macron),
1667            "HOOK_ABOVE" | "HookAbove" | "hook_above" => Ok(Self::HookAbove),
1668            "DOT_BELOW" | "DotBelow" | "dot_below" => Ok(Self::DotBelow),
1669            _ => Err(FoundationError::ParseError {
1670                type_name: "EmphasisType".to_string(),
1671                value: s.to_string(),
1672                valid_values:
1673                    "NONE, DOT_ABOVE, RING_ABOVE, TILDE, CARON, SIDE, COLON, GRAVE_ACCENT, ACUTE_ACCENT, CIRCUMFLEX, MACRON, HOOK_ABOVE, DOT_BELOW"
1674                        .to_string(),
1675            }),
1676        }
1677    }
1678}
1679
1680impl TryFrom<u8> for EmphasisType {
1681    type Error = FoundationError;
1682
1683    fn try_from(value: u8) -> Result<Self, Self::Error> {
1684        match value {
1685            0 => Ok(Self::None),
1686            1 => Ok(Self::DotAbove),
1687            2 => Ok(Self::RingAbove),
1688            3 => Ok(Self::Tilde),
1689            4 => Ok(Self::Caron),
1690            5 => Ok(Self::Side),
1691            6 => Ok(Self::Colon),
1692            7 => Ok(Self::GraveAccent),
1693            8 => Ok(Self::AcuteAccent),
1694            9 => Ok(Self::Circumflex),
1695            10 => Ok(Self::Macron),
1696            11 => Ok(Self::HookAbove),
1697            12 => Ok(Self::DotBelow),
1698            _ => Err(FoundationError::ParseError {
1699                type_name: "EmphasisType".to_string(),
1700                value: value.to_string(),
1701                valid_values: "0-12 (None through DotBelow)".to_string(),
1702            }),
1703        }
1704    }
1705}
1706
1707impl schemars::JsonSchema for EmphasisType {
1708    fn schema_name() -> std::borrow::Cow<'static, str> {
1709        std::borrow::Cow::Borrowed("EmphasisType")
1710    }
1711
1712    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1713        gen.subschema_for::<String>()
1714    }
1715}
1716
1717// ---------------------------------------------------------------------------
1718// HeadingType
1719// ---------------------------------------------------------------------------
1720
1721/// Paragraph heading type for outline/numbering classification.
1722///
1723/// Controls how a paragraph participates in document outline or numbering.
1724/// Maps to the HWPX `<hh:heading type="...">` attribute.
1725///
1726/// # Examples
1727///
1728/// ```
1729/// use hwpforge_foundation::HeadingType;
1730///
1731/// assert_eq!(HeadingType::default(), HeadingType::None);
1732/// ```
1733#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1734#[non_exhaustive]
1735#[repr(u8)]
1736pub enum HeadingType {
1737    /// No heading (body text, default).
1738    #[default]
1739    None = 0,
1740    /// Outline heading (개요).
1741    Outline = 1,
1742    /// Number heading.
1743    Number = 2,
1744    /// Bullet heading.
1745    Bullet = 3,
1746}
1747
1748impl HeadingType {
1749    /// Converts to the HWPX XML attribute string.
1750    pub fn to_hwpx_str(self) -> &'static str {
1751        match self {
1752            Self::None => "NONE",
1753            Self::Outline => "OUTLINE",
1754            Self::Number => "NUMBER",
1755            Self::Bullet => "BULLET",
1756        }
1757    }
1758
1759    /// Parses a HWPX XML attribute string.
1760    pub fn from_hwpx_str(s: &str) -> Self {
1761        match s {
1762            "NONE" => Self::None,
1763            "OUTLINE" => Self::Outline,
1764            "NUMBER" => Self::Number,
1765            "BULLET" => Self::Bullet,
1766            _ => Self::None,
1767        }
1768    }
1769}
1770
1771impl fmt::Display for HeadingType {
1772    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1773        match self {
1774            Self::None => f.write_str("None"),
1775            Self::Outline => f.write_str("Outline"),
1776            Self::Number => f.write_str("Number"),
1777            Self::Bullet => f.write_str("Bullet"),
1778        }
1779    }
1780}
1781
1782impl std::str::FromStr for HeadingType {
1783    type Err = FoundationError;
1784
1785    fn from_str(s: &str) -> Result<Self, Self::Err> {
1786        match s {
1787            "None" | "none" | "NONE" => Ok(Self::None),
1788            "Outline" | "outline" | "OUTLINE" => Ok(Self::Outline),
1789            "Number" | "number" | "NUMBER" => Ok(Self::Number),
1790            "Bullet" | "bullet" | "BULLET" => Ok(Self::Bullet),
1791            _ => Err(FoundationError::ParseError {
1792                type_name: "HeadingType".to_string(),
1793                value: s.to_string(),
1794                valid_values: "None, Outline, Number, Bullet".to_string(),
1795            }),
1796        }
1797    }
1798}
1799
1800impl TryFrom<u8> for HeadingType {
1801    type Error = FoundationError;
1802
1803    fn try_from(value: u8) -> Result<Self, Self::Error> {
1804        match value {
1805            0 => Ok(Self::None),
1806            1 => Ok(Self::Outline),
1807            2 => Ok(Self::Number),
1808            3 => Ok(Self::Bullet),
1809            _ => Err(FoundationError::ParseError {
1810                type_name: "HeadingType".to_string(),
1811                value: value.to_string(),
1812                valid_values: "0 (None), 1 (Outline), 2 (Number), 3 (Bullet)".to_string(),
1813            }),
1814        }
1815    }
1816}
1817
1818impl schemars::JsonSchema for HeadingType {
1819    fn schema_name() -> std::borrow::Cow<'static, str> {
1820        std::borrow::Cow::Borrowed("HeadingType")
1821    }
1822
1823    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1824        gen.subschema_for::<String>()
1825    }
1826}
1827
1828// ---------------------------------------------------------------------------
1829// TabAlign
1830// ---------------------------------------------------------------------------
1831
1832/// Tab stop alignment.
1833///
1834/// Maps to HWPX `<hh:tabItem type="...">`.
1835#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1836#[non_exhaustive]
1837#[repr(u8)]
1838pub enum TabAlign {
1839    /// Left-aligned tab.
1840    #[default]
1841    Left = 0,
1842    /// Right-aligned tab.
1843    Right = 1,
1844    /// Center-aligned tab.
1845    Center = 2,
1846    /// Decimal-aligned tab.
1847    Decimal = 3,
1848}
1849
1850impl TabAlign {
1851    /// Converts to the HWPX XML attribute string.
1852    pub fn to_hwpx_str(self) -> &'static str {
1853        match self {
1854            Self::Left => "LEFT",
1855            Self::Right => "RIGHT",
1856            Self::Center => "CENTER",
1857            Self::Decimal => "DECIMAL",
1858        }
1859    }
1860
1861    /// Parses a HWPX XML attribute string.
1862    pub fn from_hwpx_str(s: &str) -> Self {
1863        match s {
1864            "RIGHT" => Self::Right,
1865            "CENTER" => Self::Center,
1866            "DECIMAL" => Self::Decimal,
1867            _ => Self::Left,
1868        }
1869    }
1870}
1871
1872impl fmt::Display for TabAlign {
1873    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1874        match self {
1875            Self::Left => f.write_str("Left"),
1876            Self::Right => f.write_str("Right"),
1877            Self::Center => f.write_str("Center"),
1878            Self::Decimal => f.write_str("Decimal"),
1879        }
1880    }
1881}
1882
1883impl std::str::FromStr for TabAlign {
1884    type Err = FoundationError;
1885
1886    fn from_str(s: &str) -> Result<Self, Self::Err> {
1887        match s {
1888            "Left" | "LEFT" | "left" => Ok(Self::Left),
1889            "Right" | "RIGHT" | "right" => Ok(Self::Right),
1890            "Center" | "CENTER" | "center" => Ok(Self::Center),
1891            "Decimal" | "DECIMAL" | "decimal" => Ok(Self::Decimal),
1892            _ => Err(FoundationError::ParseError {
1893                type_name: "TabAlign".to_string(),
1894                value: s.to_string(),
1895                valid_values: "Left, Right, Center, Decimal".to_string(),
1896            }),
1897        }
1898    }
1899}
1900
1901impl schemars::JsonSchema for TabAlign {
1902    fn schema_name() -> std::borrow::Cow<'static, str> {
1903        std::borrow::Cow::Borrowed("TabAlign")
1904    }
1905
1906    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1907        gen.subschema_for::<String>()
1908    }
1909}
1910
1911// ---------------------------------------------------------------------------
1912// TabLeader
1913// ---------------------------------------------------------------------------
1914
1915/// Tab leader line style.
1916///
1917/// Stored as an uppercase HWPX-compatible string so unknown vendor values
1918/// survive roundtrip instead of being silently flattened.
1919#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1920#[serde(transparent)]
1921pub struct TabLeader(String);
1922
1923impl TabLeader {
1924    /// Creates a leader from a HWPX line type string.
1925    pub fn from_hwpx_str(s: &str) -> Self {
1926        Self(s.to_ascii_uppercase())
1927    }
1928
1929    /// Returns the canonical HWPX string.
1930    pub fn as_hwpx_str(&self) -> &str {
1931        &self.0
1932    }
1933
1934    /// No leader.
1935    pub fn none() -> Self {
1936        Self::from_hwpx_str("NONE")
1937    }
1938
1939    /// Dotted leader.
1940    pub fn dot() -> Self {
1941        Self::from_hwpx_str("DOT")
1942    }
1943}
1944
1945impl fmt::Display for TabLeader {
1946    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1947        f.write_str(self.as_hwpx_str())
1948    }
1949}
1950
1951impl std::str::FromStr for TabLeader {
1952    type Err = FoundationError;
1953
1954    fn from_str(s: &str) -> Result<Self, Self::Err> {
1955        Ok(Self::from_hwpx_str(s))
1956    }
1957}
1958
1959impl schemars::JsonSchema for TabLeader {
1960    fn schema_name() -> std::borrow::Cow<'static, str> {
1961        std::borrow::Cow::Borrowed("TabLeader")
1962    }
1963
1964    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1965        gen.subschema_for::<String>()
1966    }
1967}
1968
1969// ---------------------------------------------------------------------------
1970// GutterType
1971// ---------------------------------------------------------------------------
1972
1973/// Gutter position type for page margins.
1974///
1975/// Controls where the binding gutter space is placed on the page.
1976/// Used in `<hp:pagePr gutterType="...">`.
1977///
1978/// # Examples
1979///
1980/// ```
1981/// use hwpforge_foundation::GutterType;
1982///
1983/// assert_eq!(GutterType::default(), GutterType::LeftOnly);
1984/// assert_eq!(GutterType::LeftOnly.to_string(), "LeftOnly");
1985/// ```
1986#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1987#[non_exhaustive]
1988#[repr(u8)]
1989pub enum GutterType {
1990    /// Gutter on the left side only (default).
1991    #[default]
1992    LeftOnly = 0,
1993    /// Gutter on the left and right sides.
1994    LeftRight = 1,
1995    /// Gutter on the top side only.
1996    TopOnly = 2,
1997    /// Gutter on the top and bottom sides.
1998    TopBottom = 3,
1999}
2000
2001impl fmt::Display for GutterType {
2002    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2003        match self {
2004            Self::LeftOnly => f.write_str("LeftOnly"),
2005            Self::LeftRight => f.write_str("LeftRight"),
2006            Self::TopOnly => f.write_str("TopOnly"),
2007            Self::TopBottom => f.write_str("TopBottom"),
2008        }
2009    }
2010}
2011
2012impl std::str::FromStr for GutterType {
2013    type Err = FoundationError;
2014
2015    fn from_str(s: &str) -> Result<Self, Self::Err> {
2016        match s {
2017            "LeftOnly" | "LEFT_ONLY" | "left_only" => Ok(Self::LeftOnly),
2018            "LeftRight" | "LEFT_RIGHT" | "left_right" => Ok(Self::LeftRight),
2019            "TopOnly" | "TOP_ONLY" | "top_only" => Ok(Self::TopOnly),
2020            "TopBottom" | "TOP_BOTTOM" | "top_bottom" => Ok(Self::TopBottom),
2021            _ => Err(FoundationError::ParseError {
2022                type_name: "GutterType".to_string(),
2023                value: s.to_string(),
2024                valid_values: "LeftOnly, LeftRight, TopOnly, TopBottom".to_string(),
2025            }),
2026        }
2027    }
2028}
2029
2030impl TryFrom<u8> for GutterType {
2031    type Error = FoundationError;
2032
2033    fn try_from(value: u8) -> Result<Self, Self::Error> {
2034        match value {
2035            0 => Ok(Self::LeftOnly),
2036            1 => Ok(Self::LeftRight),
2037            2 => Ok(Self::TopOnly),
2038            3 => Ok(Self::TopBottom),
2039            _ => Err(FoundationError::ParseError {
2040                type_name: "GutterType".to_string(),
2041                value: value.to_string(),
2042                valid_values: "0 (LeftOnly), 1 (LeftRight), 2 (TopOnly), 3 (TopBottom)".to_string(),
2043            }),
2044        }
2045    }
2046}
2047
2048impl schemars::JsonSchema for GutterType {
2049    fn schema_name() -> std::borrow::Cow<'static, str> {
2050        std::borrow::Cow::Borrowed("GutterType")
2051    }
2052
2053    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2054        gen.subschema_for::<String>()
2055    }
2056}
2057
2058// ---------------------------------------------------------------------------
2059// ShowMode
2060// ---------------------------------------------------------------------------
2061
2062/// Visibility mode for page borders and fills.
2063///
2064/// Controls on which pages the border or fill is displayed.
2065/// Used in `<hp:visibility border="..." fill="...">`.
2066///
2067/// # Examples
2068///
2069/// ```
2070/// use hwpforge_foundation::ShowMode;
2071///
2072/// assert_eq!(ShowMode::default(), ShowMode::ShowAll);
2073/// assert_eq!(ShowMode::ShowAll.to_string(), "ShowAll");
2074/// ```
2075#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2076#[non_exhaustive]
2077#[repr(u8)]
2078pub enum ShowMode {
2079    /// Show on all pages (default).
2080    #[default]
2081    ShowAll = 0,
2082    /// Hide on all pages.
2083    HideAll = 1,
2084    /// Show on odd pages only.
2085    ShowOdd = 2,
2086    /// Show on even pages only.
2087    ShowEven = 3,
2088}
2089
2090impl fmt::Display for ShowMode {
2091    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2092        match self {
2093            Self::ShowAll => f.write_str("ShowAll"),
2094            Self::HideAll => f.write_str("HideAll"),
2095            Self::ShowOdd => f.write_str("ShowOdd"),
2096            Self::ShowEven => f.write_str("ShowEven"),
2097        }
2098    }
2099}
2100
2101impl std::str::FromStr for ShowMode {
2102    type Err = FoundationError;
2103
2104    fn from_str(s: &str) -> Result<Self, Self::Err> {
2105        match s {
2106            "ShowAll" | "SHOW_ALL" | "show_all" => Ok(Self::ShowAll),
2107            "HideAll" | "HIDE_ALL" | "hide_all" => Ok(Self::HideAll),
2108            "ShowOdd" | "SHOW_ODD" | "show_odd" => Ok(Self::ShowOdd),
2109            "ShowEven" | "SHOW_EVEN" | "show_even" => Ok(Self::ShowEven),
2110            _ => Err(FoundationError::ParseError {
2111                type_name: "ShowMode".to_string(),
2112                value: s.to_string(),
2113                valid_values: "ShowAll, HideAll, ShowOdd, ShowEven".to_string(),
2114            }),
2115        }
2116    }
2117}
2118
2119impl TryFrom<u8> for ShowMode {
2120    type Error = FoundationError;
2121
2122    fn try_from(value: u8) -> Result<Self, Self::Error> {
2123        match value {
2124            0 => Ok(Self::ShowAll),
2125            1 => Ok(Self::HideAll),
2126            2 => Ok(Self::ShowOdd),
2127            3 => Ok(Self::ShowEven),
2128            _ => Err(FoundationError::ParseError {
2129                type_name: "ShowMode".to_string(),
2130                value: value.to_string(),
2131                valid_values: "0 (ShowAll), 1 (HideAll), 2 (ShowOdd), 3 (ShowEven)".to_string(),
2132            }),
2133        }
2134    }
2135}
2136
2137impl schemars::JsonSchema for ShowMode {
2138    fn schema_name() -> std::borrow::Cow<'static, str> {
2139        std::borrow::Cow::Borrowed("ShowMode")
2140    }
2141
2142    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2143        gen.subschema_for::<String>()
2144    }
2145}
2146
2147// ---------------------------------------------------------------------------
2148// RestartType
2149// ---------------------------------------------------------------------------
2150
2151/// Line number restart type.
2152///
2153/// Controls when line numbering restarts to 1.
2154/// Used in `<hp:lineNumberShape restartType="...">`.
2155///
2156/// # Examples
2157///
2158/// ```
2159/// use hwpforge_foundation::RestartType;
2160///
2161/// assert_eq!(RestartType::default(), RestartType::Continuous);
2162/// assert_eq!(RestartType::Continuous.to_string(), "Continuous");
2163/// ```
2164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2165#[non_exhaustive]
2166#[repr(u8)]
2167pub enum RestartType {
2168    /// Continuous numbering throughout the document (default).
2169    #[default]
2170    Continuous = 0,
2171    /// Restart numbering at each section.
2172    Section = 1,
2173    /// Restart numbering at each page.
2174    Page = 2,
2175}
2176
2177impl fmt::Display for RestartType {
2178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2179        match self {
2180            Self::Continuous => f.write_str("Continuous"),
2181            Self::Section => f.write_str("Section"),
2182            Self::Page => f.write_str("Page"),
2183        }
2184    }
2185}
2186
2187impl std::str::FromStr for RestartType {
2188    type Err = FoundationError;
2189
2190    fn from_str(s: &str) -> Result<Self, Self::Err> {
2191        match s {
2192            "Continuous" | "continuous" | "0" => Ok(Self::Continuous),
2193            "Section" | "section" | "1" => Ok(Self::Section),
2194            "Page" | "page" | "2" => Ok(Self::Page),
2195            _ => Err(FoundationError::ParseError {
2196                type_name: "RestartType".to_string(),
2197                value: s.to_string(),
2198                valid_values: "Continuous, Section, Page".to_string(),
2199            }),
2200        }
2201    }
2202}
2203
2204impl TryFrom<u8> for RestartType {
2205    type Error = FoundationError;
2206
2207    fn try_from(value: u8) -> Result<Self, Self::Error> {
2208        match value {
2209            0 => Ok(Self::Continuous),
2210            1 => Ok(Self::Section),
2211            2 => Ok(Self::Page),
2212            _ => Err(FoundationError::ParseError {
2213                type_name: "RestartType".to_string(),
2214                value: value.to_string(),
2215                valid_values: "0 (Continuous), 1 (Section), 2 (Page)".to_string(),
2216            }),
2217        }
2218    }
2219}
2220
2221impl schemars::JsonSchema for RestartType {
2222    fn schema_name() -> std::borrow::Cow<'static, str> {
2223        std::borrow::Cow::Borrowed("RestartType")
2224    }
2225
2226    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2227        gen.subschema_for::<String>()
2228    }
2229}
2230
2231// ---------------------------------------------------------------------------
2232// TextBorderType
2233// ---------------------------------------------------------------------------
2234
2235/// Reference frame for page border offset measurement.
2236///
2237/// Controls whether page border offsets are measured from the paper edge
2238/// or from the content area.
2239///
2240/// # Examples
2241///
2242/// ```
2243/// use hwpforge_foundation::TextBorderType;
2244///
2245/// assert_eq!(TextBorderType::default(), TextBorderType::Paper);
2246/// assert_eq!(TextBorderType::Paper.to_string(), "Paper");
2247/// ```
2248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2249#[non_exhaustive]
2250#[repr(u8)]
2251pub enum TextBorderType {
2252    /// Offsets measured from paper edge (default).
2253    #[default]
2254    Paper = 0,
2255    /// Offsets measured from content area.
2256    Content = 1,
2257}
2258
2259impl fmt::Display for TextBorderType {
2260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2261        match self {
2262            Self::Paper => f.write_str("Paper"),
2263            Self::Content => f.write_str("Content"),
2264        }
2265    }
2266}
2267
2268impl std::str::FromStr for TextBorderType {
2269    type Err = FoundationError;
2270
2271    fn from_str(s: &str) -> Result<Self, Self::Err> {
2272        match s {
2273            "Paper" | "PAPER" | "paper" => Ok(Self::Paper),
2274            "Content" | "CONTENT" | "content" => Ok(Self::Content),
2275            _ => Err(FoundationError::ParseError {
2276                type_name: "TextBorderType".to_string(),
2277                value: s.to_string(),
2278                valid_values: "Paper, Content".to_string(),
2279            }),
2280        }
2281    }
2282}
2283
2284impl TryFrom<u8> for TextBorderType {
2285    type Error = FoundationError;
2286
2287    fn try_from(value: u8) -> Result<Self, Self::Error> {
2288        match value {
2289            0 => Ok(Self::Paper),
2290            1 => Ok(Self::Content),
2291            _ => Err(FoundationError::ParseError {
2292                type_name: "TextBorderType".to_string(),
2293                value: value.to_string(),
2294                valid_values: "0 (Paper), 1 (Content)".to_string(),
2295            }),
2296        }
2297    }
2298}
2299
2300impl schemars::JsonSchema for TextBorderType {
2301    fn schema_name() -> std::borrow::Cow<'static, str> {
2302        std::borrow::Cow::Borrowed("TextBorderType")
2303    }
2304
2305    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2306        gen.subschema_for::<String>()
2307    }
2308}
2309
2310// ---------------------------------------------------------------------------
2311// Flip
2312// ---------------------------------------------------------------------------
2313
2314/// Flip/mirror state for drawing shapes.
2315///
2316/// Controls horizontal and/or vertical mirroring of a shape.
2317///
2318/// # Examples
2319///
2320/// ```
2321/// use hwpforge_foundation::Flip;
2322///
2323/// assert_eq!(Flip::default(), Flip::None);
2324/// assert_eq!(Flip::Horizontal.to_string(), "Horizontal");
2325/// ```
2326#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2327#[non_exhaustive]
2328#[repr(u8)]
2329pub enum Flip {
2330    /// No flip (default).
2331    #[default]
2332    None = 0,
2333    /// Mirrored horizontally.
2334    Horizontal = 1,
2335    /// Mirrored vertically.
2336    Vertical = 2,
2337    /// Mirrored both horizontally and vertically.
2338    Both = 3,
2339}
2340
2341impl fmt::Display for Flip {
2342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2343        match self {
2344            Self::None => f.write_str("None"),
2345            Self::Horizontal => f.write_str("Horizontal"),
2346            Self::Vertical => f.write_str("Vertical"),
2347            Self::Both => f.write_str("Both"),
2348        }
2349    }
2350}
2351
2352impl std::str::FromStr for Flip {
2353    type Err = FoundationError;
2354
2355    fn from_str(s: &str) -> Result<Self, Self::Err> {
2356        match s {
2357            "None" | "NONE" | "none" => Ok(Self::None),
2358            "Horizontal" | "HORIZONTAL" | "horizontal" => Ok(Self::Horizontal),
2359            "Vertical" | "VERTICAL" | "vertical" => Ok(Self::Vertical),
2360            "Both" | "BOTH" | "both" => Ok(Self::Both),
2361            _ => Err(FoundationError::ParseError {
2362                type_name: "Flip".to_string(),
2363                value: s.to_string(),
2364                valid_values: "None, Horizontal, Vertical, Both".to_string(),
2365            }),
2366        }
2367    }
2368}
2369
2370impl TryFrom<u8> for Flip {
2371    type Error = FoundationError;
2372
2373    fn try_from(value: u8) -> Result<Self, Self::Error> {
2374        match value {
2375            0 => Ok(Self::None),
2376            1 => Ok(Self::Horizontal),
2377            2 => Ok(Self::Vertical),
2378            3 => Ok(Self::Both),
2379            _ => Err(FoundationError::ParseError {
2380                type_name: "Flip".to_string(),
2381                value: value.to_string(),
2382                valid_values: "0 (None), 1 (Horizontal), 2 (Vertical), 3 (Both)".to_string(),
2383            }),
2384        }
2385    }
2386}
2387
2388impl schemars::JsonSchema for Flip {
2389    fn schema_name() -> std::borrow::Cow<'static, str> {
2390        std::borrow::Cow::Borrowed("Flip")
2391    }
2392
2393    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2394        gen.subschema_for::<String>()
2395    }
2396}
2397
2398// ---------------------------------------------------------------------------
2399// ArcType
2400// ---------------------------------------------------------------------------
2401
2402/// Arc drawing type for ellipse-based arc shapes.
2403///
2404/// # Examples
2405///
2406/// ```
2407/// use hwpforge_foundation::ArcType;
2408///
2409/// assert_eq!(ArcType::default(), ArcType::Normal);
2410/// ```
2411#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2412#[non_exhaustive]
2413#[repr(u8)]
2414pub enum ArcType {
2415    /// Open arc (just the curved edge).
2416    #[default]
2417    Normal = 0,
2418    /// Pie/sector (arc + two radii closing to center).
2419    Pie = 1,
2420    /// Chord (arc + straight line closing endpoints).
2421    Chord = 2,
2422}
2423
2424impl fmt::Display for ArcType {
2425    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2426        match self {
2427            Self::Normal => f.write_str("NORMAL"),
2428            Self::Pie => f.write_str("PIE"),
2429            Self::Chord => f.write_str("CHORD"),
2430        }
2431    }
2432}
2433
2434impl std::str::FromStr for ArcType {
2435    type Err = FoundationError;
2436
2437    fn from_str(s: &str) -> Result<Self, Self::Err> {
2438        match s {
2439            "NORMAL" | "Normal" | "normal" => Ok(Self::Normal),
2440            "PIE" | "Pie" | "pie" => Ok(Self::Pie),
2441            "CHORD" | "Chord" | "chord" => Ok(Self::Chord),
2442            _ => Err(FoundationError::ParseError {
2443                type_name: "ArcType".to_string(),
2444                value: s.to_string(),
2445                valid_values: "NORMAL, PIE, CHORD".to_string(),
2446            }),
2447        }
2448    }
2449}
2450
2451impl TryFrom<u8> for ArcType {
2452    type Error = FoundationError;
2453
2454    fn try_from(value: u8) -> Result<Self, Self::Error> {
2455        match value {
2456            0 => Ok(Self::Normal),
2457            1 => Ok(Self::Pie),
2458            2 => Ok(Self::Chord),
2459            _ => Err(FoundationError::ParseError {
2460                type_name: "ArcType".to_string(),
2461                value: value.to_string(),
2462                valid_values: "0 (Normal), 1 (Pie), 2 (Chord)".to_string(),
2463            }),
2464        }
2465    }
2466}
2467
2468impl schemars::JsonSchema for ArcType {
2469    fn schema_name() -> std::borrow::Cow<'static, str> {
2470        std::borrow::Cow::Borrowed("ArcType")
2471    }
2472
2473    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2474        gen.subschema_for::<String>()
2475    }
2476}
2477
2478// ---------------------------------------------------------------------------
2479// ArrowType
2480// ---------------------------------------------------------------------------
2481
2482/// Arrowhead shape for line endpoints.
2483///
2484/// # Examples
2485///
2486/// ```
2487/// use hwpforge_foundation::ArrowType;
2488///
2489/// assert_eq!(ArrowType::default(), ArrowType::None);
2490/// ```
2491#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2492#[non_exhaustive]
2493#[repr(u8)]
2494pub enum ArrowType {
2495    /// No arrowhead (default).
2496    #[default]
2497    None = 0,
2498    /// Standard filled arrowhead.
2499    Normal = 1,
2500    /// Arrow-shaped arrowhead.
2501    Arrow = 2,
2502    /// Concave arrowhead.
2503    Concave = 3,
2504    /// Diamond arrowhead.
2505    Diamond = 4,
2506    /// Oval/circle arrowhead.
2507    Oval = 5,
2508    /// Open (unfilled) arrowhead.
2509    Open = 6,
2510}
2511
2512impl fmt::Display for ArrowType {
2513    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2514        // KS X 6101 ArrowType values.
2515        // Diamond/Oval/Open default to FILLED_ variants here;
2516        // the encoder resolves FILLED_ vs EMPTY_ based on ArrowStyle.filled.
2517        match self {
2518            Self::None => f.write_str("NORMAL"),
2519            Self::Normal => f.write_str("ARROW"),
2520            Self::Arrow => f.write_str("SPEAR"),
2521            Self::Concave => f.write_str("CONCAVE_ARROW"),
2522            Self::Diamond => f.write_str("FILLED_DIAMOND"),
2523            Self::Oval => f.write_str("FILLED_CIRCLE"),
2524            Self::Open => f.write_str("EMPTY_BOX"),
2525        }
2526    }
2527}
2528
2529impl std::str::FromStr for ArrowType {
2530    type Err = FoundationError;
2531
2532    fn from_str(s: &str) -> Result<Self, Self::Err> {
2533        // KS X 6101 ArrowType values (primary) + legacy aliases for backward compat.
2534        match s {
2535            "NORMAL" => Ok(Self::None),
2536            "ARROW" => Ok(Self::Normal),
2537            "SPEAR" => Ok(Self::Arrow),
2538            "CONCAVE_ARROW" => Ok(Self::Concave),
2539            "FILLED_DIAMOND" | "EMPTY_DIAMOND" => Ok(Self::Diamond),
2540            "FILLED_CIRCLE" | "EMPTY_CIRCLE" => Ok(Self::Oval),
2541            "FILLED_BOX" | "EMPTY_BOX" => Ok(Self::Open),
2542            _ => Err(FoundationError::ParseError {
2543                type_name: "ArrowType".to_string(),
2544                value: s.to_string(),
2545                valid_values: "NORMAL, ARROW, SPEAR, CONCAVE_ARROW, FILLED_DIAMOND, EMPTY_DIAMOND, FILLED_CIRCLE, EMPTY_CIRCLE, FILLED_BOX, EMPTY_BOX"
2546                    .to_string(),
2547            }),
2548        }
2549    }
2550}
2551
2552impl TryFrom<u8> for ArrowType {
2553    type Error = FoundationError;
2554
2555    fn try_from(value: u8) -> Result<Self, Self::Error> {
2556        match value {
2557            0 => Ok(Self::None),
2558            1 => Ok(Self::Normal),
2559            2 => Ok(Self::Arrow),
2560            3 => Ok(Self::Concave),
2561            4 => Ok(Self::Diamond),
2562            5 => Ok(Self::Oval),
2563            6 => Ok(Self::Open),
2564            _ => Err(FoundationError::ParseError {
2565                type_name: "ArrowType".to_string(),
2566                value: value.to_string(),
2567                valid_values:
2568                    "0 (None), 1 (Normal), 2 (Arrow), 3 (Concave), 4 (Diamond), 5 (Oval), 6 (Open)"
2569                        .to_string(),
2570            }),
2571        }
2572    }
2573}
2574
2575impl schemars::JsonSchema for ArrowType {
2576    fn schema_name() -> std::borrow::Cow<'static, str> {
2577        std::borrow::Cow::Borrowed("ArrowType")
2578    }
2579
2580    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2581        gen.subschema_for::<String>()
2582    }
2583}
2584
2585// ---------------------------------------------------------------------------
2586// ArrowSize
2587// ---------------------------------------------------------------------------
2588
2589/// Arrowhead size for line endpoints.
2590///
2591/// Encoded as `{HEAD}_{TAIL}` string in HWPX (e.g. `"MEDIUM_MEDIUM"`).
2592///
2593/// # Examples
2594///
2595/// ```
2596/// use hwpforge_foundation::ArrowSize;
2597///
2598/// assert_eq!(ArrowSize::default(), ArrowSize::Medium);
2599/// ```
2600#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2601#[non_exhaustive]
2602#[repr(u8)]
2603pub enum ArrowSize {
2604    /// Small arrowhead.
2605    Small = 0,
2606    /// Medium arrowhead (default).
2607    #[default]
2608    Medium = 1,
2609    /// Large arrowhead.
2610    Large = 2,
2611}
2612
2613impl fmt::Display for ArrowSize {
2614    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2615        match self {
2616            Self::Small => f.write_str("SMALL_SMALL"),
2617            Self::Medium => f.write_str("MEDIUM_MEDIUM"),
2618            Self::Large => f.write_str("LARGE_LARGE"),
2619        }
2620    }
2621}
2622
2623impl std::str::FromStr for ArrowSize {
2624    type Err = FoundationError;
2625
2626    fn from_str(s: &str) -> Result<Self, Self::Err> {
2627        match s {
2628            "SMALL_SMALL" | "Small" | "small" => Ok(Self::Small),
2629            "MEDIUM_MEDIUM" | "Medium" | "medium" => Ok(Self::Medium),
2630            "LARGE_LARGE" | "Large" | "large" => Ok(Self::Large),
2631            _ => Err(FoundationError::ParseError {
2632                type_name: "ArrowSize".to_string(),
2633                value: s.to_string(),
2634                valid_values: "SMALL_SMALL, MEDIUM_MEDIUM, LARGE_LARGE".to_string(),
2635            }),
2636        }
2637    }
2638}
2639
2640impl TryFrom<u8> for ArrowSize {
2641    type Error = FoundationError;
2642
2643    fn try_from(value: u8) -> Result<Self, Self::Error> {
2644        match value {
2645            0 => Ok(Self::Small),
2646            1 => Ok(Self::Medium),
2647            2 => Ok(Self::Large),
2648            _ => Err(FoundationError::ParseError {
2649                type_name: "ArrowSize".to_string(),
2650                value: value.to_string(),
2651                valid_values: "0 (Small), 1 (Medium), 2 (Large)".to_string(),
2652            }),
2653        }
2654    }
2655}
2656
2657impl schemars::JsonSchema for ArrowSize {
2658    fn schema_name() -> std::borrow::Cow<'static, str> {
2659        std::borrow::Cow::Borrowed("ArrowSize")
2660    }
2661
2662    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2663        gen.subschema_for::<String>()
2664    }
2665}
2666
2667// ---------------------------------------------------------------------------
2668// GradientType
2669// ---------------------------------------------------------------------------
2670
2671/// Gradient fill direction type.
2672///
2673/// # Examples
2674///
2675/// ```
2676/// use hwpforge_foundation::GradientType;
2677///
2678/// assert_eq!(GradientType::default(), GradientType::Linear);
2679/// ```
2680#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2681#[non_exhaustive]
2682#[repr(u8)]
2683pub enum GradientType {
2684    /// Linear gradient (default).
2685    #[default]
2686    Linear = 0,
2687    /// Radial gradient (from center outward).
2688    Radial = 1,
2689    /// Square/rectangular gradient.
2690    Square = 2,
2691    /// Conical gradient (angular sweep).
2692    Conical = 3,
2693}
2694
2695impl fmt::Display for GradientType {
2696    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2697        match self {
2698            Self::Linear => f.write_str("LINEAR"),
2699            Self::Radial => f.write_str("RADIAL"),
2700            Self::Square => f.write_str("SQUARE"),
2701            Self::Conical => f.write_str("CONICAL"),
2702        }
2703    }
2704}
2705
2706impl std::str::FromStr for GradientType {
2707    type Err = FoundationError;
2708
2709    fn from_str(s: &str) -> Result<Self, Self::Err> {
2710        match s {
2711            "LINEAR" | "Linear" | "linear" => Ok(Self::Linear),
2712            "RADIAL" | "Radial" | "radial" => Ok(Self::Radial),
2713            "SQUARE" | "Square" | "square" => Ok(Self::Square),
2714            "CONICAL" | "Conical" | "conical" => Ok(Self::Conical),
2715            _ => Err(FoundationError::ParseError {
2716                type_name: "GradientType".to_string(),
2717                value: s.to_string(),
2718                valid_values: "LINEAR, RADIAL, SQUARE, CONICAL".to_string(),
2719            }),
2720        }
2721    }
2722}
2723
2724impl TryFrom<u8> for GradientType {
2725    type Error = FoundationError;
2726
2727    fn try_from(value: u8) -> Result<Self, Self::Error> {
2728        match value {
2729            0 => Ok(Self::Linear),
2730            1 => Ok(Self::Radial),
2731            2 => Ok(Self::Square),
2732            3 => Ok(Self::Conical),
2733            _ => Err(FoundationError::ParseError {
2734                type_name: "GradientType".to_string(),
2735                value: value.to_string(),
2736                valid_values: "0 (Linear), 1 (Radial), 2 (Square), 3 (Conical)".to_string(),
2737            }),
2738        }
2739    }
2740}
2741
2742impl schemars::JsonSchema for GradientType {
2743    fn schema_name() -> std::borrow::Cow<'static, str> {
2744        std::borrow::Cow::Borrowed("GradientType")
2745    }
2746
2747    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2748        gen.subschema_for::<String>()
2749    }
2750}
2751
2752// ---------------------------------------------------------------------------
2753// PatternType
2754// ---------------------------------------------------------------------------
2755
2756/// Hatch/pattern fill type for shapes.
2757///
2758/// # Examples
2759///
2760/// ```
2761/// use hwpforge_foundation::PatternType;
2762///
2763/// assert_eq!(PatternType::default(), PatternType::Horizontal);
2764/// ```
2765#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2766#[non_exhaustive]
2767#[repr(u8)]
2768pub enum PatternType {
2769    /// Horizontal lines (default).
2770    #[default]
2771    Horizontal = 0,
2772    /// Vertical lines.
2773    Vertical = 1,
2774    /// Backslash diagonal lines.
2775    BackSlash = 2,
2776    /// Forward slash diagonal lines.
2777    Slash = 3,
2778    /// Cross-hatch (horizontal + vertical).
2779    Cross = 4,
2780    /// Cross-diagonal hatch.
2781    CrossDiagonal = 5,
2782}
2783
2784impl fmt::Display for PatternType {
2785    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2786        // 한글 renders BACK_SLASH as `/` and SLASH as `\` — opposite to KS X 6101 XSD docs.
2787        // We swap the mapping so our semantic enum values match actual rendering.
2788        match self {
2789            Self::Horizontal => f.write_str("HORIZONTAL"),
2790            Self::Vertical => f.write_str("VERTICAL"),
2791            Self::BackSlash => f.write_str("SLASH"),
2792            Self::Slash => f.write_str("BACK_SLASH"),
2793            Self::Cross => f.write_str("CROSS"),
2794            Self::CrossDiagonal => f.write_str("CROSS_DIAGONAL"),
2795        }
2796    }
2797}
2798
2799impl std::str::FromStr for PatternType {
2800    type Err = FoundationError;
2801
2802    fn from_str(s: &str) -> Result<Self, Self::Err> {
2803        // Swapped BACK_SLASH/SLASH to match Display (한글 renders them opposite to spec).
2804        // Only SCREAMING_CASE forms used here — PascalCase comes through serde derive.
2805        match s {
2806            "HORIZONTAL" | "horizontal" => Ok(Self::Horizontal),
2807            "VERTICAL" | "vertical" => Ok(Self::Vertical),
2808            "BACK_SLASH" | "backslash" => Ok(Self::Slash),
2809            "SLASH" | "slash" => Ok(Self::BackSlash),
2810            "CROSS" | "cross" => Ok(Self::Cross),
2811            "CROSS_DIAGONAL" | "crossdiagonal" => Ok(Self::CrossDiagonal),
2812            _ => Err(FoundationError::ParseError {
2813                type_name: "PatternType".to_string(),
2814                value: s.to_string(),
2815                valid_values: "HORIZONTAL, VERTICAL, BACK_SLASH, SLASH, CROSS, CROSS_DIAGONAL"
2816                    .to_string(),
2817            }),
2818        }
2819    }
2820}
2821
2822impl TryFrom<u8> for PatternType {
2823    type Error = FoundationError;
2824
2825    fn try_from(value: u8) -> Result<Self, Self::Error> {
2826        match value {
2827            0 => Ok(Self::Horizontal),
2828            1 => Ok(Self::Vertical),
2829            2 => Ok(Self::BackSlash),
2830            3 => Ok(Self::Slash),
2831            4 => Ok(Self::Cross),
2832            5 => Ok(Self::CrossDiagonal),
2833            _ => Err(FoundationError::ParseError {
2834                type_name: "PatternType".to_string(),
2835                value: value.to_string(),
2836                valid_values:
2837                    "0 (Horizontal), 1 (Vertical), 2 (BackSlash), 3 (Slash), 4 (Cross), 5 (CrossDiagonal)"
2838                        .to_string(),
2839            }),
2840        }
2841    }
2842}
2843
2844impl schemars::JsonSchema for PatternType {
2845    fn schema_name() -> std::borrow::Cow<'static, str> {
2846        std::borrow::Cow::Borrowed("PatternType")
2847    }
2848
2849    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2850        gen.subschema_for::<String>()
2851    }
2852}
2853
2854// ---------------------------------------------------------------------------
2855// ImageFillMode
2856// ---------------------------------------------------------------------------
2857
2858/// How an image is fitted within a shape fill area.
2859///
2860/// # Examples
2861///
2862/// ```
2863/// use hwpforge_foundation::ImageFillMode;
2864///
2865/// assert_eq!(ImageFillMode::default(), ImageFillMode::Tile);
2866/// ```
2867#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2868#[non_exhaustive]
2869#[repr(u8)]
2870pub enum ImageFillMode {
2871    /// Tile the image to fill the area (default).
2872    #[default]
2873    Tile = 0,
2874    /// Center the image without scaling.
2875    Center = 1,
2876    /// Stretch the image to fit exactly.
2877    Stretch = 2,
2878    /// Scale proportionally to fit all within the area.
2879    FitAll = 3,
2880}
2881
2882impl fmt::Display for ImageFillMode {
2883    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2884        match self {
2885            Self::Tile => f.write_str("TILE"),
2886            Self::Center => f.write_str("CENTER"),
2887            Self::Stretch => f.write_str("STRETCH"),
2888            Self::FitAll => f.write_str("FIT_ALL"),
2889        }
2890    }
2891}
2892
2893impl std::str::FromStr for ImageFillMode {
2894    type Err = FoundationError;
2895
2896    fn from_str(s: &str) -> Result<Self, Self::Err> {
2897        match s {
2898            "TILE" | "Tile" | "tile" => Ok(Self::Tile),
2899            "CENTER" | "Center" | "center" => Ok(Self::Center),
2900            "STRETCH" | "Stretch" | "stretch" => Ok(Self::Stretch),
2901            "FIT_ALL" | "FitAll" | "fit_all" => Ok(Self::FitAll),
2902            _ => Err(FoundationError::ParseError {
2903                type_name: "ImageFillMode".to_string(),
2904                value: s.to_string(),
2905                valid_values: "TILE, CENTER, STRETCH, FIT_ALL".to_string(),
2906            }),
2907        }
2908    }
2909}
2910
2911impl TryFrom<u8> for ImageFillMode {
2912    type Error = FoundationError;
2913
2914    fn try_from(value: u8) -> Result<Self, Self::Error> {
2915        match value {
2916            0 => Ok(Self::Tile),
2917            1 => Ok(Self::Center),
2918            2 => Ok(Self::Stretch),
2919            3 => Ok(Self::FitAll),
2920            _ => Err(FoundationError::ParseError {
2921                type_name: "ImageFillMode".to_string(),
2922                value: value.to_string(),
2923                valid_values: "0 (Tile), 1 (Center), 2 (Stretch), 3 (FitAll)".to_string(),
2924            }),
2925        }
2926    }
2927}
2928
2929impl schemars::JsonSchema for ImageFillMode {
2930    fn schema_name() -> std::borrow::Cow<'static, str> {
2931        std::borrow::Cow::Borrowed("ImageFillMode")
2932    }
2933
2934    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2935        gen.subschema_for::<String>()
2936    }
2937}
2938
2939// ---------------------------------------------------------------------------
2940// CurveSegmentType
2941// ---------------------------------------------------------------------------
2942
2943/// Segment type within a curve path.
2944///
2945/// # Examples
2946///
2947/// ```
2948/// use hwpforge_foundation::CurveSegmentType;
2949///
2950/// assert_eq!(CurveSegmentType::default(), CurveSegmentType::Line);
2951/// ```
2952#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2953#[non_exhaustive]
2954#[repr(u8)]
2955pub enum CurveSegmentType {
2956    /// Straight line segment (default).
2957    #[default]
2958    Line = 0,
2959    /// Cubic bezier curve segment.
2960    Curve = 1,
2961}
2962
2963impl fmt::Display for CurveSegmentType {
2964    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2965        match self {
2966            Self::Line => f.write_str("LINE"),
2967            Self::Curve => f.write_str("CURVE"),
2968        }
2969    }
2970}
2971
2972impl std::str::FromStr for CurveSegmentType {
2973    type Err = FoundationError;
2974
2975    fn from_str(s: &str) -> Result<Self, Self::Err> {
2976        match s {
2977            "LINE" | "Line" | "line" => Ok(Self::Line),
2978            "CURVE" | "Curve" | "curve" => Ok(Self::Curve),
2979            _ => Err(FoundationError::ParseError {
2980                type_name: "CurveSegmentType".to_string(),
2981                value: s.to_string(),
2982                valid_values: "LINE, CURVE".to_string(),
2983            }),
2984        }
2985    }
2986}
2987
2988impl TryFrom<u8> for CurveSegmentType {
2989    type Error = FoundationError;
2990
2991    fn try_from(value: u8) -> Result<Self, Self::Error> {
2992        match value {
2993            0 => Ok(Self::Line),
2994            1 => Ok(Self::Curve),
2995            _ => Err(FoundationError::ParseError {
2996                type_name: "CurveSegmentType".to_string(),
2997                value: value.to_string(),
2998                valid_values: "0 (Line), 1 (Curve)".to_string(),
2999            }),
3000        }
3001    }
3002}
3003
3004impl schemars::JsonSchema for CurveSegmentType {
3005    fn schema_name() -> std::borrow::Cow<'static, str> {
3006        std::borrow::Cow::Borrowed("CurveSegmentType")
3007    }
3008
3009    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3010        gen.subschema_for::<String>()
3011    }
3012}
3013
3014// ---------------------------------------------------------------------------
3015// BookmarkType
3016// ---------------------------------------------------------------------------
3017
3018/// Type of bookmark in an HWPX document.
3019///
3020/// Bookmarks can mark a single point or span a range of content
3021/// (start/end pair using `fieldBegin`/`fieldEnd`).
3022#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
3023#[non_exhaustive]
3024#[repr(u8)]
3025pub enum BookmarkType {
3026    /// A point bookmark at a single location (direct serde in `<hp:ctrl>`).
3027    #[default]
3028    Point = 0,
3029    /// Start of a span bookmark (`fieldBegin type="BOOKMARK"`).
3030    SpanStart = 1,
3031    /// End of a span bookmark (`fieldEnd beginIDRef`).
3032    SpanEnd = 2,
3033}
3034
3035impl fmt::Display for BookmarkType {
3036    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3037        match self {
3038            Self::Point => f.write_str("Point"),
3039            Self::SpanStart => f.write_str("SpanStart"),
3040            Self::SpanEnd => f.write_str("SpanEnd"),
3041        }
3042    }
3043}
3044
3045impl std::str::FromStr for BookmarkType {
3046    type Err = FoundationError;
3047
3048    fn from_str(s: &str) -> Result<Self, Self::Err> {
3049        match s {
3050            "Point" | "point" => Ok(Self::Point),
3051            "SpanStart" | "span_start" => Ok(Self::SpanStart),
3052            "SpanEnd" | "span_end" => Ok(Self::SpanEnd),
3053            _ => Err(FoundationError::ParseError {
3054                type_name: "BookmarkType".to_string(),
3055                value: s.to_string(),
3056                valid_values: "Point, SpanStart, SpanEnd".to_string(),
3057            }),
3058        }
3059    }
3060}
3061
3062impl TryFrom<u8> for BookmarkType {
3063    type Error = FoundationError;
3064
3065    fn try_from(value: u8) -> Result<Self, Self::Error> {
3066        match value {
3067            0 => Ok(Self::Point),
3068            1 => Ok(Self::SpanStart),
3069            2 => Ok(Self::SpanEnd),
3070            _ => Err(FoundationError::ParseError {
3071                type_name: "BookmarkType".to_string(),
3072                value: value.to_string(),
3073                valid_values: "0 (Point), 1 (SpanStart), 2 (SpanEnd)".to_string(),
3074            }),
3075        }
3076    }
3077}
3078
3079impl schemars::JsonSchema for BookmarkType {
3080    fn schema_name() -> std::borrow::Cow<'static, str> {
3081        std::borrow::Cow::Borrowed("BookmarkType")
3082    }
3083
3084    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3085        gen.subschema_for::<String>()
3086    }
3087}
3088
3089// ---------------------------------------------------------------------------
3090// FieldType
3091// ---------------------------------------------------------------------------
3092
3093/// Type of a press-field (누름틀) in an HWPX document.
3094///
3095/// Press-fields are interactive form fields that users can click to fill in.
3096#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
3097#[non_exhaustive]
3098#[repr(u8)]
3099pub enum FieldType {
3100    /// Click-here placeholder field (default).
3101    #[default]
3102    ClickHere = 0,
3103    /// Automatic date field.
3104    Date = 1,
3105    /// Automatic time field.
3106    Time = 2,
3107    /// Page number field.
3108    PageNum = 3,
3109    /// Document summary field.
3110    DocSummary = 4,
3111    /// User information field.
3112    UserInfo = 5,
3113}
3114
3115impl fmt::Display for FieldType {
3116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3117        match self {
3118            Self::ClickHere => f.write_str("CLICK_HERE"),
3119            Self::Date => f.write_str("DATE"),
3120            Self::Time => f.write_str("TIME"),
3121            Self::PageNum => f.write_str("PAGE_NUM"),
3122            Self::DocSummary => f.write_str("DOC_SUMMARY"),
3123            Self::UserInfo => f.write_str("USER_INFO"),
3124        }
3125    }
3126}
3127
3128impl std::str::FromStr for FieldType {
3129    type Err = FoundationError;
3130
3131    fn from_str(s: &str) -> Result<Self, Self::Err> {
3132        match s {
3133            "CLICK_HERE" | "ClickHere" | "click_here" => Ok(Self::ClickHere),
3134            "DATE" | "Date" | "date" => Ok(Self::Date),
3135            "TIME" | "Time" | "time" => Ok(Self::Time),
3136            "PAGE_NUM" | "PageNum" | "page_num" => Ok(Self::PageNum),
3137            "DOC_SUMMARY" | "DocSummary" | "doc_summary" => Ok(Self::DocSummary),
3138            "USER_INFO" | "UserInfo" | "user_info" => Ok(Self::UserInfo),
3139            _ => Err(FoundationError::ParseError {
3140                type_name: "FieldType".to_string(),
3141                value: s.to_string(),
3142                valid_values: "CLICK_HERE, DATE, TIME, PAGE_NUM, DOC_SUMMARY, USER_INFO"
3143                    .to_string(),
3144            }),
3145        }
3146    }
3147}
3148
3149impl TryFrom<u8> for FieldType {
3150    type Error = FoundationError;
3151
3152    fn try_from(value: u8) -> Result<Self, Self::Error> {
3153        match value {
3154            0 => Ok(Self::ClickHere),
3155            1 => Ok(Self::Date),
3156            2 => Ok(Self::Time),
3157            3 => Ok(Self::PageNum),
3158            4 => Ok(Self::DocSummary),
3159            5 => Ok(Self::UserInfo),
3160            _ => Err(FoundationError::ParseError {
3161                type_name: "FieldType".to_string(),
3162                value: value.to_string(),
3163                valid_values: "0..5 (ClickHere..UserInfo)".to_string(),
3164            }),
3165        }
3166    }
3167}
3168
3169impl schemars::JsonSchema for FieldType {
3170    fn schema_name() -> std::borrow::Cow<'static, str> {
3171        std::borrow::Cow::Borrowed("FieldType")
3172    }
3173
3174    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3175        gen.subschema_for::<String>()
3176    }
3177}
3178
3179// ---------------------------------------------------------------------------
3180// RefType
3181// ---------------------------------------------------------------------------
3182
3183/// Target type of a cross-reference (상호참조).
3184#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
3185#[non_exhaustive]
3186#[repr(u8)]
3187pub enum RefType {
3188    /// Reference to a bookmark target.
3189    #[default]
3190    Bookmark = 0,
3191    /// Reference to a table caption number.
3192    Table = 1,
3193    /// Reference to a figure/image caption number.
3194    Figure = 2,
3195    /// Reference to an equation number.
3196    Equation = 3,
3197}
3198
3199impl fmt::Display for RefType {
3200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3201        match self {
3202            Self::Bookmark => f.write_str("TARGET_BOOKMARK"),
3203            Self::Table => f.write_str("TARGET_TABLE"),
3204            Self::Figure => f.write_str("TARGET_FIGURE"),
3205            Self::Equation => f.write_str("TARGET_EQUATION"),
3206        }
3207    }
3208}
3209
3210impl std::str::FromStr for RefType {
3211    type Err = FoundationError;
3212
3213    fn from_str(s: &str) -> Result<Self, Self::Err> {
3214        match s {
3215            "TARGET_BOOKMARK" | "Bookmark" | "bookmark" => Ok(Self::Bookmark),
3216            "TARGET_TABLE" | "Table" | "table" => Ok(Self::Table),
3217            "TARGET_FIGURE" | "Figure" | "figure" => Ok(Self::Figure),
3218            "TARGET_EQUATION" | "Equation" | "equation" => Ok(Self::Equation),
3219            _ => Err(FoundationError::ParseError {
3220                type_name: "RefType".to_string(),
3221                value: s.to_string(),
3222                valid_values: "TARGET_BOOKMARK, TARGET_TABLE, TARGET_FIGURE, TARGET_EQUATION"
3223                    .to_string(),
3224            }),
3225        }
3226    }
3227}
3228
3229impl TryFrom<u8> for RefType {
3230    type Error = FoundationError;
3231
3232    fn try_from(value: u8) -> Result<Self, Self::Error> {
3233        match value {
3234            0 => Ok(Self::Bookmark),
3235            1 => Ok(Self::Table),
3236            2 => Ok(Self::Figure),
3237            3 => Ok(Self::Equation),
3238            _ => Err(FoundationError::ParseError {
3239                type_name: "RefType".to_string(),
3240                value: value.to_string(),
3241                valid_values: "0..3 (Bookmark..Equation)".to_string(),
3242            }),
3243        }
3244    }
3245}
3246
3247impl schemars::JsonSchema for RefType {
3248    fn schema_name() -> std::borrow::Cow<'static, str> {
3249        std::borrow::Cow::Borrowed("RefType")
3250    }
3251
3252    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3253        gen.subschema_for::<String>()
3254    }
3255}
3256
3257// ---------------------------------------------------------------------------
3258// RefContentType
3259// ---------------------------------------------------------------------------
3260
3261/// Content display type for a cross-reference (what to show at the reference site).
3262#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
3263#[non_exhaustive]
3264#[repr(u8)]
3265pub enum RefContentType {
3266    /// Show page number where the target appears.
3267    #[default]
3268    Page = 0,
3269    /// Show the target's numbering (e.g. "표 3", "그림 2").
3270    Number = 1,
3271    /// Show the target's content text.
3272    Contents = 2,
3273    /// Show relative position ("위" / "아래").
3274    UpDownPos = 3,
3275}
3276
3277impl fmt::Display for RefContentType {
3278    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3279        match self {
3280            Self::Page => f.write_str("OBJECT_TYPE_PAGE"),
3281            Self::Number => f.write_str("OBJECT_TYPE_NUMBER"),
3282            Self::Contents => f.write_str("OBJECT_TYPE_CONTENTS"),
3283            Self::UpDownPos => f.write_str("OBJECT_TYPE_UPDOWNPOS"),
3284        }
3285    }
3286}
3287
3288impl std::str::FromStr for RefContentType {
3289    type Err = FoundationError;
3290
3291    fn from_str(s: &str) -> Result<Self, Self::Err> {
3292        match s {
3293            "OBJECT_TYPE_PAGE" | "Page" | "page" => Ok(Self::Page),
3294            "OBJECT_TYPE_NUMBER" | "Number" | "number" => Ok(Self::Number),
3295            "OBJECT_TYPE_CONTENTS" | "Contents" | "contents" => Ok(Self::Contents),
3296            "OBJECT_TYPE_UPDOWNPOS" | "UpDownPos" | "updownpos" => Ok(Self::UpDownPos),
3297            _ => Err(FoundationError::ParseError {
3298                type_name: "RefContentType".to_string(),
3299                value: s.to_string(),
3300                valid_values:
3301                    "OBJECT_TYPE_PAGE, OBJECT_TYPE_NUMBER, OBJECT_TYPE_CONTENTS, OBJECT_TYPE_UPDOWNPOS"
3302                        .to_string(),
3303            }),
3304        }
3305    }
3306}
3307
3308impl TryFrom<u8> for RefContentType {
3309    type Error = FoundationError;
3310
3311    fn try_from(value: u8) -> Result<Self, Self::Error> {
3312        match value {
3313            0 => Ok(Self::Page),
3314            1 => Ok(Self::Number),
3315            2 => Ok(Self::Contents),
3316            3 => Ok(Self::UpDownPos),
3317            _ => Err(FoundationError::ParseError {
3318                type_name: "RefContentType".to_string(),
3319                value: value.to_string(),
3320                valid_values: "0..3 (Page..UpDownPos)".to_string(),
3321            }),
3322        }
3323    }
3324}
3325
3326impl schemars::JsonSchema for RefContentType {
3327    fn schema_name() -> std::borrow::Cow<'static, str> {
3328        std::borrow::Cow::Borrowed("RefContentType")
3329    }
3330
3331    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3332        gen.subschema_for::<String>()
3333    }
3334}
3335
3336/// Drop cap style for floating shape objects (HWPX `dropcapstyle` attribute).
3337///
3338/// Controls whether a shape (text box, image, table, etc.) is formatted as a
3339/// drop capital that occupies multiple lines at the start of a paragraph.
3340///
3341/// # HWPX Values
3342///
3343/// | Variant      | HWPX string     |
3344/// |--------------|-----------------|
3345/// | `None`       | `"None"`        |
3346/// | `DoubleLine` | `"DoubleLine"`  |
3347/// | `TripleLine` | `"TripleLine"`  |
3348/// | `Margin`     | `"Margin"`      |
3349#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
3350#[non_exhaustive]
3351pub enum DropCapStyle {
3352    /// No drop cap (default).
3353    #[default]
3354    None = 0,
3355    /// Drop cap spanning 2 lines.
3356    DoubleLine = 1,
3357    /// Drop cap spanning 3 lines.
3358    TripleLine = 2,
3359    /// Drop cap positioned in the margin.
3360    Margin = 3,
3361}
3362
3363impl fmt::Display for DropCapStyle {
3364    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3365        match self {
3366            Self::None => f.write_str("None"),
3367            Self::DoubleLine => f.write_str("DoubleLine"),
3368            Self::TripleLine => f.write_str("TripleLine"),
3369            Self::Margin => f.write_str("Margin"),
3370        }
3371    }
3372}
3373
3374impl DropCapStyle {
3375    /// Parses an HWPX `dropcapstyle` attribute value (PascalCase).
3376    ///
3377    /// Unknown values fall back to `None` (default) for forward compatibility.
3378    pub fn from_hwpx_str(s: &str) -> Self {
3379        match s {
3380            "DoubleLine" => Self::DoubleLine,
3381            "TripleLine" => Self::TripleLine,
3382            "Margin" => Self::Margin,
3383            _ => Self::None,
3384        }
3385    }
3386}
3387
3388impl std::str::FromStr for DropCapStyle {
3389    type Err = FoundationError;
3390
3391    fn from_str(s: &str) -> Result<Self, Self::Err> {
3392        match s {
3393            "None" | "NONE" | "none" => Ok(Self::None),
3394            "DoubleLine" | "DOUBLE_LINE" => Ok(Self::DoubleLine),
3395            "TripleLine" | "TRIPLE_LINE" => Ok(Self::TripleLine),
3396            "Margin" | "MARGIN" => Ok(Self::Margin),
3397            _ => Err(FoundationError::ParseError {
3398                type_name: "DropCapStyle".to_string(),
3399                value: s.to_string(),
3400                valid_values: "None, DoubleLine, TripleLine, Margin".to_string(),
3401            }),
3402        }
3403    }
3404}
3405
3406impl schemars::JsonSchema for DropCapStyle {
3407    fn schema_name() -> std::borrow::Cow<'static, str> {
3408        std::borrow::Cow::Borrowed("DropCapStyle")
3409    }
3410
3411    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3412        gen.subschema_for::<String>()
3413    }
3414}
3415
3416impl serde::Serialize for DropCapStyle {
3417    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3418        serializer.serialize_str(&self.to_string())
3419    }
3420}
3421
3422impl<'de> serde::Deserialize<'de> for DropCapStyle {
3423    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3424        let s = String::deserialize(deserializer)?;
3425        s.parse().map_err(serde::de::Error::custom)
3426    }
3427}
3428
3429// Compile-time size assertions: all enums are 1 byte
3430const _: () = assert!(std::mem::size_of::<DropCapStyle>() == 1);
3431const _: () = assert!(std::mem::size_of::<Alignment>() == 1);
3432const _: () = assert!(std::mem::size_of::<LineSpacingType>() == 1);
3433const _: () = assert!(std::mem::size_of::<BreakType>() == 1);
3434const _: () = assert!(std::mem::size_of::<Language>() == 1);
3435const _: () = assert!(std::mem::size_of::<UnderlineType>() == 1);
3436const _: () = assert!(std::mem::size_of::<StrikeoutShape>() == 1);
3437const _: () = assert!(std::mem::size_of::<OutlineType>() == 1);
3438const _: () = assert!(std::mem::size_of::<ShadowType>() == 1);
3439const _: () = assert!(std::mem::size_of::<EmbossType>() == 1);
3440const _: () = assert!(std::mem::size_of::<EngraveType>() == 1);
3441const _: () = assert!(std::mem::size_of::<VerticalPosition>() == 1);
3442const _: () = assert!(std::mem::size_of::<BorderLineType>() == 1);
3443const _: () = assert!(std::mem::size_of::<FillBrushType>() == 1);
3444const _: () = assert!(std::mem::size_of::<ApplyPageType>() == 1);
3445const _: () = assert!(std::mem::size_of::<NumberFormatType>() == 1);
3446const _: () = assert!(std::mem::size_of::<PageNumberPosition>() == 1);
3447const _: () = assert!(std::mem::size_of::<WordBreakType>() == 1);
3448const _: () = assert!(std::mem::size_of::<EmphasisType>() == 1);
3449const _: () = assert!(std::mem::size_of::<HeadingType>() == 1);
3450const _: () = assert!(std::mem::size_of::<GutterType>() == 1);
3451const _: () = assert!(std::mem::size_of::<ShowMode>() == 1);
3452const _: () = assert!(std::mem::size_of::<RestartType>() == 1);
3453const _: () = assert!(std::mem::size_of::<TextBorderType>() == 1);
3454const _: () = assert!(std::mem::size_of::<Flip>() == 1);
3455const _: () = assert!(std::mem::size_of::<ArcType>() == 1);
3456const _: () = assert!(std::mem::size_of::<ArrowType>() == 1);
3457const _: () = assert!(std::mem::size_of::<ArrowSize>() == 1);
3458const _: () = assert!(std::mem::size_of::<GradientType>() == 1);
3459const _: () = assert!(std::mem::size_of::<PatternType>() == 1);
3460const _: () = assert!(std::mem::size_of::<ImageFillMode>() == 1);
3461const _: () = assert!(std::mem::size_of::<CurveSegmentType>() == 1);
3462const _: () = assert!(std::mem::size_of::<BookmarkType>() == 1);
3463const _: () = assert!(std::mem::size_of::<FieldType>() == 1);
3464const _: () = assert!(std::mem::size_of::<RefType>() == 1);
3465const _: () = assert!(std::mem::size_of::<RefContentType>() == 1);
3466
3467// ---------------------------------------------------------------------------
3468// TextDirection
3469// ---------------------------------------------------------------------------
3470
3471/// Text writing direction for sections and sub-lists.
3472///
3473/// Controls whether text flows horizontally (가로쓰기) or vertically (세로쓰기).
3474/// Used in `<hp:secPr textDirection="...">` and `<hp:subList textDirection="...">`.
3475///
3476/// # Examples
3477///
3478/// ```
3479/// use hwpforge_foundation::TextDirection;
3480///
3481/// assert_eq!(TextDirection::default(), TextDirection::Horizontal);
3482/// assert_eq!(TextDirection::Horizontal.to_string(), "HORIZONTAL");
3483/// ```
3484#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
3485#[non_exhaustive]
3486pub enum TextDirection {
3487    /// Horizontal writing (가로쓰기) — default.
3488    #[default]
3489    Horizontal,
3490    /// Vertical writing with Latin chars rotated 90° (세로쓰기 영문 눕힘).
3491    Vertical,
3492    /// Vertical writing with Latin chars upright (세로쓰기 영문 세움).
3493    VerticalAll,
3494}
3495
3496impl TextDirection {
3497    /// Parses a HWPX XML attribute string (e.g. `"VERTICAL"`).
3498    ///
3499    /// Unknown values fall back to [`TextDirection::Horizontal`].
3500    pub fn from_hwpx_str(s: &str) -> Self {
3501        match s {
3502            "VERTICAL" => Self::Vertical,
3503            "VERTICALALL" => Self::VerticalAll,
3504            _ => Self::Horizontal,
3505        }
3506    }
3507}
3508
3509impl fmt::Display for TextDirection {
3510    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3511        match self {
3512            Self::Horizontal => f.write_str("HORIZONTAL"),
3513            Self::Vertical => f.write_str("VERTICAL"),
3514            Self::VerticalAll => f.write_str("VERTICALALL"),
3515        }
3516    }
3517}
3518
3519impl schemars::JsonSchema for TextDirection {
3520    fn schema_name() -> std::borrow::Cow<'static, str> {
3521        std::borrow::Cow::Borrowed("TextDirection")
3522    }
3523
3524    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3525        gen.subschema_for::<String>()
3526    }
3527}
3528
3529const _: () = assert!(std::mem::size_of::<TextDirection>() == 1);
3530
3531#[cfg(test)]
3532mod tests {
3533    use super::*;
3534    use std::str::FromStr;
3535
3536    // ===================================================================
3537    // Alignment (10+ tests)
3538    // ===================================================================
3539
3540    #[test]
3541    fn alignment_default_is_left() {
3542        assert_eq!(Alignment::default(), Alignment::Left);
3543    }
3544
3545    #[test]
3546    fn alignment_display_all_variants() {
3547        assert_eq!(Alignment::Left.to_string(), "Left");
3548        assert_eq!(Alignment::Center.to_string(), "Center");
3549        assert_eq!(Alignment::Right.to_string(), "Right");
3550        assert_eq!(Alignment::Justify.to_string(), "Justify");
3551        assert_eq!(Alignment::Distribute.to_string(), "Distribute");
3552        assert_eq!(Alignment::DistributeFlush.to_string(), "DistributeFlush");
3553    }
3554
3555    #[test]
3556    fn alignment_from_str_pascal_case() {
3557        assert_eq!(Alignment::from_str("Left").unwrap(), Alignment::Left);
3558        assert_eq!(Alignment::from_str("Center").unwrap(), Alignment::Center);
3559        assert_eq!(Alignment::from_str("Right").unwrap(), Alignment::Right);
3560        assert_eq!(Alignment::from_str("Justify").unwrap(), Alignment::Justify);
3561        assert_eq!(Alignment::from_str("Distribute").unwrap(), Alignment::Distribute);
3562        assert_eq!(Alignment::from_str("DistributeFlush").unwrap(), Alignment::DistributeFlush);
3563    }
3564
3565    #[test]
3566    fn alignment_from_str_lower_case() {
3567        assert_eq!(Alignment::from_str("left").unwrap(), Alignment::Left);
3568        assert_eq!(Alignment::from_str("center").unwrap(), Alignment::Center);
3569        assert_eq!(Alignment::from_str("distribute").unwrap(), Alignment::Distribute);
3570        assert_eq!(Alignment::from_str("distributeflush").unwrap(), Alignment::DistributeFlush);
3571        assert_eq!(Alignment::from_str("distribute_flush").unwrap(), Alignment::DistributeFlush);
3572    }
3573
3574    #[test]
3575    fn alignment_from_str_invalid() {
3576        let err = Alignment::from_str("leftt").unwrap_err();
3577        match err {
3578            FoundationError::ParseError { ref type_name, ref value, .. } => {
3579                assert_eq!(type_name, "Alignment");
3580                assert_eq!(value, "leftt");
3581            }
3582            other => panic!("unexpected: {other}"),
3583        }
3584    }
3585
3586    #[test]
3587    fn alignment_try_from_u8() {
3588        assert_eq!(Alignment::try_from(0u8).unwrap(), Alignment::Left);
3589        assert_eq!(Alignment::try_from(1u8).unwrap(), Alignment::Center);
3590        assert_eq!(Alignment::try_from(2u8).unwrap(), Alignment::Right);
3591        assert_eq!(Alignment::try_from(3u8).unwrap(), Alignment::Justify);
3592        assert_eq!(Alignment::try_from(4u8).unwrap(), Alignment::Distribute);
3593        assert_eq!(Alignment::try_from(5u8).unwrap(), Alignment::DistributeFlush);
3594        assert!(Alignment::try_from(6u8).is_err());
3595        assert!(Alignment::try_from(255u8).is_err());
3596    }
3597
3598    #[test]
3599    fn alignment_repr_values() {
3600        assert_eq!(Alignment::Left as u8, 0);
3601        assert_eq!(Alignment::Center as u8, 1);
3602        assert_eq!(Alignment::Right as u8, 2);
3603        assert_eq!(Alignment::Justify as u8, 3);
3604        assert_eq!(Alignment::Distribute as u8, 4);
3605        assert_eq!(Alignment::DistributeFlush as u8, 5);
3606    }
3607
3608    #[test]
3609    fn alignment_serde_roundtrip() {
3610        for variant in &[
3611            Alignment::Left,
3612            Alignment::Center,
3613            Alignment::Right,
3614            Alignment::Justify,
3615            Alignment::Distribute,
3616            Alignment::DistributeFlush,
3617        ] {
3618            let json = serde_json::to_string(variant).unwrap();
3619            let back: Alignment = serde_json::from_str(&json).unwrap();
3620            assert_eq!(&back, variant);
3621        }
3622    }
3623
3624    #[test]
3625    fn alignment_str_roundtrip() {
3626        for variant in &[
3627            Alignment::Left,
3628            Alignment::Center,
3629            Alignment::Right,
3630            Alignment::Justify,
3631            Alignment::Distribute,
3632            Alignment::DistributeFlush,
3633        ] {
3634            let s = variant.to_string();
3635            let back = Alignment::from_str(&s).unwrap();
3636            assert_eq!(&back, variant);
3637        }
3638    }
3639
3640    #[test]
3641    fn alignment_copy_and_hash() {
3642        use std::collections::HashSet;
3643        let a = Alignment::Left;
3644        let b = a; // Copy
3645        assert_eq!(a, b);
3646
3647        let mut set = HashSet::new();
3648        set.insert(Alignment::Left);
3649        set.insert(Alignment::Right);
3650        assert_eq!(set.len(), 2);
3651    }
3652
3653    // ===================================================================
3654    // LineSpacingType
3655    // ===================================================================
3656
3657    #[test]
3658    fn line_spacing_default_is_percentage() {
3659        assert_eq!(LineSpacingType::default(), LineSpacingType::Percentage);
3660    }
3661
3662    #[test]
3663    fn line_spacing_display() {
3664        assert_eq!(LineSpacingType::Percentage.to_string(), "Percentage");
3665        assert_eq!(LineSpacingType::Fixed.to_string(), "Fixed");
3666        assert_eq!(LineSpacingType::BetweenLines.to_string(), "BetweenLines");
3667    }
3668
3669    #[test]
3670    fn line_spacing_from_str() {
3671        assert_eq!(LineSpacingType::from_str("Percentage").unwrap(), LineSpacingType::Percentage);
3672        assert_eq!(LineSpacingType::from_str("Fixed").unwrap(), LineSpacingType::Fixed);
3673        assert_eq!(
3674            LineSpacingType::from_str("BetweenLines").unwrap(),
3675            LineSpacingType::BetweenLines
3676        );
3677        assert!(LineSpacingType::from_str("invalid").is_err());
3678    }
3679
3680    #[test]
3681    fn line_spacing_try_from_u8() {
3682        assert_eq!(LineSpacingType::try_from(0u8).unwrap(), LineSpacingType::Percentage);
3683        assert_eq!(LineSpacingType::try_from(1u8).unwrap(), LineSpacingType::Fixed);
3684        assert_eq!(LineSpacingType::try_from(2u8).unwrap(), LineSpacingType::BetweenLines);
3685        assert!(LineSpacingType::try_from(3u8).is_err());
3686    }
3687
3688    #[test]
3689    fn line_spacing_str_roundtrip() {
3690        for v in
3691            &[LineSpacingType::Percentage, LineSpacingType::Fixed, LineSpacingType::BetweenLines]
3692        {
3693            let s = v.to_string();
3694            let back = LineSpacingType::from_str(&s).unwrap();
3695            assert_eq!(&back, v);
3696        }
3697    }
3698
3699    // ===================================================================
3700    // BreakType
3701    // ===================================================================
3702
3703    #[test]
3704    fn break_type_default_is_none() {
3705        assert_eq!(BreakType::default(), BreakType::None);
3706    }
3707
3708    #[test]
3709    fn break_type_display() {
3710        assert_eq!(BreakType::None.to_string(), "None");
3711        assert_eq!(BreakType::Column.to_string(), "Column");
3712        assert_eq!(BreakType::Page.to_string(), "Page");
3713    }
3714
3715    #[test]
3716    fn break_type_from_str() {
3717        assert_eq!(BreakType::from_str("None").unwrap(), BreakType::None);
3718        assert_eq!(BreakType::from_str("Column").unwrap(), BreakType::Column);
3719        assert_eq!(BreakType::from_str("Page").unwrap(), BreakType::Page);
3720        assert!(BreakType::from_str("section").is_err());
3721    }
3722
3723    #[test]
3724    fn break_type_try_from_u8() {
3725        assert_eq!(BreakType::try_from(0u8).unwrap(), BreakType::None);
3726        assert_eq!(BreakType::try_from(1u8).unwrap(), BreakType::Column);
3727        assert_eq!(BreakType::try_from(2u8).unwrap(), BreakType::Page);
3728        assert!(BreakType::try_from(3u8).is_err());
3729    }
3730
3731    #[test]
3732    fn break_type_str_roundtrip() {
3733        for v in &[BreakType::None, BreakType::Column, BreakType::Page] {
3734            let s = v.to_string();
3735            let back = BreakType::from_str(&s).unwrap();
3736            assert_eq!(&back, v);
3737        }
3738    }
3739
3740    // ===================================================================
3741    // Language
3742    // ===================================================================
3743
3744    #[test]
3745    fn language_count_is_7() {
3746        assert_eq!(Language::COUNT, 7);
3747        assert_eq!(Language::ALL.len(), 7);
3748    }
3749
3750    #[test]
3751    fn language_default_is_korean() {
3752        assert_eq!(Language::default(), Language::Korean);
3753    }
3754
3755    #[test]
3756    fn language_discriminants() {
3757        assert_eq!(Language::Korean as u8, 0);
3758        assert_eq!(Language::English as u8, 1);
3759        assert_eq!(Language::Hanja as u8, 2);
3760        assert_eq!(Language::Japanese as u8, 3);
3761        assert_eq!(Language::Other as u8, 4);
3762        assert_eq!(Language::Symbol as u8, 5);
3763        assert_eq!(Language::User as u8, 6);
3764    }
3765
3766    #[test]
3767    fn language_display() {
3768        assert_eq!(Language::Korean.to_string(), "Korean");
3769        assert_eq!(Language::English.to_string(), "English");
3770        assert_eq!(Language::Japanese.to_string(), "Japanese");
3771    }
3772
3773    #[test]
3774    fn language_from_str() {
3775        for lang in &Language::ALL {
3776            let s = lang.to_string();
3777            let back = Language::from_str(&s).unwrap();
3778            assert_eq!(&back, lang);
3779        }
3780        assert!(Language::from_str("invalid").is_err());
3781    }
3782
3783    #[test]
3784    fn language_try_from_u8() {
3785        for (i, expected) in Language::ALL.iter().enumerate() {
3786            let parsed = Language::try_from(i as u8).unwrap();
3787            assert_eq!(&parsed, expected);
3788        }
3789        assert!(Language::try_from(7u8).is_err());
3790        assert!(Language::try_from(255u8).is_err());
3791    }
3792
3793    #[test]
3794    fn language_all_used_as_index() {
3795        // Common pattern: fonts[lang as usize]
3796        let fonts: [&str; Language::COUNT] =
3797            ["Batang", "Arial", "SimSun", "MS Mincho", "Arial", "Symbol", "Arial"];
3798        for lang in &Language::ALL {
3799            let _ = fonts[*lang as usize];
3800        }
3801    }
3802
3803    #[test]
3804    fn language_serde_roundtrip() {
3805        for lang in &Language::ALL {
3806            let json = serde_json::to_string(lang).unwrap();
3807            let back: Language = serde_json::from_str(&json).unwrap();
3808            assert_eq!(&back, lang);
3809        }
3810    }
3811
3812    // ===================================================================
3813    // UnderlineType
3814    // ===================================================================
3815
3816    #[test]
3817    fn underline_type_default_is_none() {
3818        assert_eq!(UnderlineType::default(), UnderlineType::None);
3819    }
3820
3821    #[test]
3822    fn underline_type_display() {
3823        assert_eq!(UnderlineType::None.to_string(), "None");
3824        assert_eq!(UnderlineType::Bottom.to_string(), "Bottom");
3825        assert_eq!(UnderlineType::Center.to_string(), "Center");
3826        assert_eq!(UnderlineType::Top.to_string(), "Top");
3827    }
3828
3829    #[test]
3830    fn underline_type_from_str() {
3831        assert_eq!(UnderlineType::from_str("None").unwrap(), UnderlineType::None);
3832        assert_eq!(UnderlineType::from_str("Bottom").unwrap(), UnderlineType::Bottom);
3833        assert_eq!(UnderlineType::from_str("center").unwrap(), UnderlineType::Center);
3834        assert!(UnderlineType::from_str("invalid").is_err());
3835    }
3836
3837    #[test]
3838    fn underline_type_try_from_u8() {
3839        assert_eq!(UnderlineType::try_from(0u8).unwrap(), UnderlineType::None);
3840        assert_eq!(UnderlineType::try_from(1u8).unwrap(), UnderlineType::Bottom);
3841        assert_eq!(UnderlineType::try_from(2u8).unwrap(), UnderlineType::Center);
3842        assert_eq!(UnderlineType::try_from(3u8).unwrap(), UnderlineType::Top);
3843        assert!(UnderlineType::try_from(4u8).is_err());
3844    }
3845
3846    #[test]
3847    fn underline_type_str_roundtrip() {
3848        for v in
3849            &[UnderlineType::None, UnderlineType::Bottom, UnderlineType::Center, UnderlineType::Top]
3850        {
3851            let s = v.to_string();
3852            let back = UnderlineType::from_str(&s).unwrap();
3853            assert_eq!(&back, v);
3854        }
3855    }
3856
3857    // ===================================================================
3858    // StrikeoutShape
3859    // ===================================================================
3860
3861    #[test]
3862    fn strikeout_shape_default_is_none() {
3863        assert_eq!(StrikeoutShape::default(), StrikeoutShape::None);
3864    }
3865
3866    #[test]
3867    fn strikeout_shape_display() {
3868        assert_eq!(StrikeoutShape::None.to_string(), "None");
3869        assert_eq!(StrikeoutShape::Continuous.to_string(), "Continuous");
3870        assert_eq!(StrikeoutShape::Dash.to_string(), "Dash");
3871        assert_eq!(StrikeoutShape::DashDotDot.to_string(), "DashDotDot");
3872    }
3873
3874    #[test]
3875    fn strikeout_shape_from_str() {
3876        assert_eq!(StrikeoutShape::from_str("None").unwrap(), StrikeoutShape::None);
3877        assert_eq!(StrikeoutShape::from_str("continuous").unwrap(), StrikeoutShape::Continuous);
3878        assert_eq!(StrikeoutShape::from_str("dash_dot").unwrap(), StrikeoutShape::DashDot);
3879        assert!(StrikeoutShape::from_str("invalid").is_err());
3880    }
3881
3882    #[test]
3883    fn strikeout_shape_try_from_u8() {
3884        assert_eq!(StrikeoutShape::try_from(0u8).unwrap(), StrikeoutShape::None);
3885        assert_eq!(StrikeoutShape::try_from(1u8).unwrap(), StrikeoutShape::Continuous);
3886        assert_eq!(StrikeoutShape::try_from(5u8).unwrap(), StrikeoutShape::DashDotDot);
3887        assert!(StrikeoutShape::try_from(6u8).is_err());
3888    }
3889
3890    #[test]
3891    fn strikeout_shape_str_roundtrip() {
3892        for v in &[
3893            StrikeoutShape::None,
3894            StrikeoutShape::Continuous,
3895            StrikeoutShape::Dash,
3896            StrikeoutShape::Dot,
3897            StrikeoutShape::DashDot,
3898            StrikeoutShape::DashDotDot,
3899        ] {
3900            let s = v.to_string();
3901            let back = StrikeoutShape::from_str(&s).unwrap();
3902            assert_eq!(&back, v);
3903        }
3904    }
3905
3906    // ===================================================================
3907    // OutlineType
3908    // ===================================================================
3909
3910    #[test]
3911    fn outline_type_default_is_none() {
3912        assert_eq!(OutlineType::default(), OutlineType::None);
3913    }
3914
3915    #[test]
3916    fn outline_type_display() {
3917        assert_eq!(OutlineType::None.to_string(), "None");
3918        assert_eq!(OutlineType::Solid.to_string(), "Solid");
3919    }
3920
3921    #[test]
3922    fn outline_type_from_str() {
3923        assert_eq!(OutlineType::from_str("None").unwrap(), OutlineType::None);
3924        assert_eq!(OutlineType::from_str("solid").unwrap(), OutlineType::Solid);
3925        assert!(OutlineType::from_str("dashed").is_err());
3926    }
3927
3928    #[test]
3929    fn outline_type_try_from_u8() {
3930        assert_eq!(OutlineType::try_from(0u8).unwrap(), OutlineType::None);
3931        assert_eq!(OutlineType::try_from(1u8).unwrap(), OutlineType::Solid);
3932        assert!(OutlineType::try_from(2u8).is_err());
3933    }
3934
3935    // ===================================================================
3936    // ShadowType
3937    // ===================================================================
3938
3939    #[test]
3940    fn shadow_type_default_is_none() {
3941        assert_eq!(ShadowType::default(), ShadowType::None);
3942    }
3943
3944    #[test]
3945    fn shadow_type_display() {
3946        assert_eq!(ShadowType::None.to_string(), "None");
3947        assert_eq!(ShadowType::Drop.to_string(), "Drop");
3948    }
3949
3950    #[test]
3951    fn shadow_type_from_str() {
3952        assert_eq!(ShadowType::from_str("None").unwrap(), ShadowType::None);
3953        assert_eq!(ShadowType::from_str("drop").unwrap(), ShadowType::Drop);
3954        assert!(ShadowType::from_str("shadow").is_err());
3955    }
3956
3957    #[test]
3958    fn shadow_type_try_from_u8() {
3959        assert_eq!(ShadowType::try_from(0u8).unwrap(), ShadowType::None);
3960        assert_eq!(ShadowType::try_from(1u8).unwrap(), ShadowType::Drop);
3961        assert!(ShadowType::try_from(2u8).is_err());
3962    }
3963
3964    // ===================================================================
3965    // EmbossType
3966    // ===================================================================
3967
3968    #[test]
3969    fn emboss_type_default_is_none() {
3970        assert_eq!(EmbossType::default(), EmbossType::None);
3971    }
3972
3973    #[test]
3974    fn emboss_type_display() {
3975        assert_eq!(EmbossType::None.to_string(), "None");
3976        assert_eq!(EmbossType::Emboss.to_string(), "Emboss");
3977    }
3978
3979    #[test]
3980    fn emboss_type_from_str() {
3981        assert_eq!(EmbossType::from_str("None").unwrap(), EmbossType::None);
3982        assert_eq!(EmbossType::from_str("emboss").unwrap(), EmbossType::Emboss);
3983        assert!(EmbossType::from_str("raised").is_err());
3984    }
3985
3986    #[test]
3987    fn emboss_type_try_from_u8() {
3988        assert_eq!(EmbossType::try_from(0u8).unwrap(), EmbossType::None);
3989        assert_eq!(EmbossType::try_from(1u8).unwrap(), EmbossType::Emboss);
3990        assert!(EmbossType::try_from(2u8).is_err());
3991    }
3992
3993    // ===================================================================
3994    // EngraveType
3995    // ===================================================================
3996
3997    #[test]
3998    fn engrave_type_default_is_none() {
3999        assert_eq!(EngraveType::default(), EngraveType::None);
4000    }
4001
4002    #[test]
4003    fn engrave_type_display() {
4004        assert_eq!(EngraveType::None.to_string(), "None");
4005        assert_eq!(EngraveType::Engrave.to_string(), "Engrave");
4006    }
4007
4008    #[test]
4009    fn engrave_type_from_str() {
4010        assert_eq!(EngraveType::from_str("None").unwrap(), EngraveType::None);
4011        assert_eq!(EngraveType::from_str("engrave").unwrap(), EngraveType::Engrave);
4012        assert!(EngraveType::from_str("sunken").is_err());
4013    }
4014
4015    #[test]
4016    fn engrave_type_try_from_u8() {
4017        assert_eq!(EngraveType::try_from(0u8).unwrap(), EngraveType::None);
4018        assert_eq!(EngraveType::try_from(1u8).unwrap(), EngraveType::Engrave);
4019        assert!(EngraveType::try_from(2u8).is_err());
4020    }
4021
4022    // ===================================================================
4023    // VerticalPosition
4024    // ===================================================================
4025
4026    #[test]
4027    fn vertical_position_default_is_normal() {
4028        assert_eq!(VerticalPosition::default(), VerticalPosition::Normal);
4029    }
4030
4031    #[test]
4032    fn vertical_position_display() {
4033        assert_eq!(VerticalPosition::Normal.to_string(), "Normal");
4034        assert_eq!(VerticalPosition::Superscript.to_string(), "Superscript");
4035        assert_eq!(VerticalPosition::Subscript.to_string(), "Subscript");
4036    }
4037
4038    #[test]
4039    fn vertical_position_from_str() {
4040        assert_eq!(VerticalPosition::from_str("Normal").unwrap(), VerticalPosition::Normal);
4041        assert_eq!(
4042            VerticalPosition::from_str("superscript").unwrap(),
4043            VerticalPosition::Superscript
4044        );
4045        assert_eq!(VerticalPosition::from_str("sub").unwrap(), VerticalPosition::Subscript);
4046        assert!(VerticalPosition::from_str("middle").is_err());
4047    }
4048
4049    #[test]
4050    fn vertical_position_try_from_u8() {
4051        assert_eq!(VerticalPosition::try_from(0u8).unwrap(), VerticalPosition::Normal);
4052        assert_eq!(VerticalPosition::try_from(1u8).unwrap(), VerticalPosition::Superscript);
4053        assert_eq!(VerticalPosition::try_from(2u8).unwrap(), VerticalPosition::Subscript);
4054        assert!(VerticalPosition::try_from(3u8).is_err());
4055    }
4056
4057    #[test]
4058    fn vertical_position_str_roundtrip() {
4059        for v in
4060            &[VerticalPosition::Normal, VerticalPosition::Superscript, VerticalPosition::Subscript]
4061        {
4062            let s = v.to_string();
4063            let back = VerticalPosition::from_str(&s).unwrap();
4064            assert_eq!(&back, v);
4065        }
4066    }
4067
4068    // ===================================================================
4069    // BorderLineType
4070    // ===================================================================
4071
4072    #[test]
4073    fn border_line_type_default_is_none() {
4074        assert_eq!(BorderLineType::default(), BorderLineType::None);
4075    }
4076
4077    #[test]
4078    fn border_line_type_display() {
4079        assert_eq!(BorderLineType::None.to_string(), "None");
4080        assert_eq!(BorderLineType::Solid.to_string(), "Solid");
4081        assert_eq!(BorderLineType::DashDot.to_string(), "DashDot");
4082        assert_eq!(BorderLineType::ThickBetweenSlim.to_string(), "ThickBetweenSlim");
4083    }
4084
4085    #[test]
4086    fn border_line_type_from_str() {
4087        assert_eq!(BorderLineType::from_str("None").unwrap(), BorderLineType::None);
4088        assert_eq!(BorderLineType::from_str("solid").unwrap(), BorderLineType::Solid);
4089        assert_eq!(BorderLineType::from_str("dash_dot").unwrap(), BorderLineType::DashDot);
4090        assert_eq!(BorderLineType::from_str("double").unwrap(), BorderLineType::Double);
4091        assert!(BorderLineType::from_str("wavy").is_err());
4092    }
4093
4094    #[test]
4095    fn border_line_type_try_from_u8() {
4096        assert_eq!(BorderLineType::try_from(0u8).unwrap(), BorderLineType::None);
4097        assert_eq!(BorderLineType::try_from(1u8).unwrap(), BorderLineType::Solid);
4098        assert_eq!(BorderLineType::try_from(10u8).unwrap(), BorderLineType::ThickBetweenSlim);
4099        assert!(BorderLineType::try_from(11u8).is_err());
4100    }
4101
4102    #[test]
4103    fn border_line_type_str_roundtrip() {
4104        for v in &[
4105            BorderLineType::None,
4106            BorderLineType::Solid,
4107            BorderLineType::Dash,
4108            BorderLineType::Dot,
4109            BorderLineType::DashDot,
4110            BorderLineType::DashDotDot,
4111            BorderLineType::LongDash,
4112            BorderLineType::TripleDot,
4113            BorderLineType::Double,
4114            BorderLineType::DoubleSlim,
4115            BorderLineType::ThickBetweenSlim,
4116        ] {
4117            let s = v.to_string();
4118            let back = BorderLineType::from_str(&s).unwrap();
4119            assert_eq!(&back, v);
4120        }
4121    }
4122
4123    // ===================================================================
4124    // FillBrushType
4125    // ===================================================================
4126
4127    #[test]
4128    fn fill_brush_type_default_is_none() {
4129        assert_eq!(FillBrushType::default(), FillBrushType::None);
4130    }
4131
4132    #[test]
4133    fn fill_brush_type_display() {
4134        assert_eq!(FillBrushType::None.to_string(), "None");
4135        assert_eq!(FillBrushType::Solid.to_string(), "Solid");
4136        assert_eq!(FillBrushType::Gradient.to_string(), "Gradient");
4137        assert_eq!(FillBrushType::Pattern.to_string(), "Pattern");
4138    }
4139
4140    #[test]
4141    fn fill_brush_type_from_str() {
4142        assert_eq!(FillBrushType::from_str("None").unwrap(), FillBrushType::None);
4143        assert_eq!(FillBrushType::from_str("solid").unwrap(), FillBrushType::Solid);
4144        assert_eq!(FillBrushType::from_str("gradient").unwrap(), FillBrushType::Gradient);
4145        assert!(FillBrushType::from_str("texture").is_err());
4146    }
4147
4148    #[test]
4149    fn fill_brush_type_try_from_u8() {
4150        assert_eq!(FillBrushType::try_from(0u8).unwrap(), FillBrushType::None);
4151        assert_eq!(FillBrushType::try_from(1u8).unwrap(), FillBrushType::Solid);
4152        assert_eq!(FillBrushType::try_from(2u8).unwrap(), FillBrushType::Gradient);
4153        assert_eq!(FillBrushType::try_from(3u8).unwrap(), FillBrushType::Pattern);
4154        assert!(FillBrushType::try_from(4u8).is_err());
4155    }
4156
4157    #[test]
4158    fn fill_brush_type_str_roundtrip() {
4159        for v in &[
4160            FillBrushType::None,
4161            FillBrushType::Solid,
4162            FillBrushType::Gradient,
4163            FillBrushType::Pattern,
4164        ] {
4165            let s = v.to_string();
4166            let back = FillBrushType::from_str(&s).unwrap();
4167            assert_eq!(&back, v);
4168        }
4169    }
4170
4171    // ===================================================================
4172    // Cross-enum size assertions (compile-time already, but test at runtime too)
4173    // ===================================================================
4174
4175    #[test]
4176    fn all_enums_are_one_byte() {
4177        assert_eq!(std::mem::size_of::<Alignment>(), 1);
4178        assert_eq!(std::mem::size_of::<LineSpacingType>(), 1);
4179        assert_eq!(std::mem::size_of::<BreakType>(), 1);
4180        assert_eq!(std::mem::size_of::<Language>(), 1);
4181        assert_eq!(std::mem::size_of::<UnderlineType>(), 1);
4182        assert_eq!(std::mem::size_of::<StrikeoutShape>(), 1);
4183        assert_eq!(std::mem::size_of::<OutlineType>(), 1);
4184        assert_eq!(std::mem::size_of::<ShadowType>(), 1);
4185        assert_eq!(std::mem::size_of::<EmbossType>(), 1);
4186        assert_eq!(std::mem::size_of::<EngraveType>(), 1);
4187        assert_eq!(std::mem::size_of::<VerticalPosition>(), 1);
4188        assert_eq!(std::mem::size_of::<BorderLineType>(), 1);
4189        assert_eq!(std::mem::size_of::<FillBrushType>(), 1);
4190        assert_eq!(std::mem::size_of::<ApplyPageType>(), 1);
4191        assert_eq!(std::mem::size_of::<NumberFormatType>(), 1);
4192        assert_eq!(std::mem::size_of::<PageNumberPosition>(), 1);
4193    }
4194
4195    // ===================================================================
4196    // ApplyPageType
4197    // ===================================================================
4198
4199    #[test]
4200    fn apply_page_type_default_is_both() {
4201        assert_eq!(ApplyPageType::default(), ApplyPageType::Both);
4202    }
4203
4204    #[test]
4205    fn apply_page_type_display() {
4206        assert_eq!(ApplyPageType::Both.to_string(), "Both");
4207        assert_eq!(ApplyPageType::Even.to_string(), "Even");
4208        assert_eq!(ApplyPageType::Odd.to_string(), "Odd");
4209    }
4210
4211    #[test]
4212    fn apply_page_type_from_str() {
4213        assert_eq!(ApplyPageType::from_str("Both").unwrap(), ApplyPageType::Both);
4214        assert_eq!(ApplyPageType::from_str("BOTH").unwrap(), ApplyPageType::Both);
4215        assert_eq!(ApplyPageType::from_str("even").unwrap(), ApplyPageType::Even);
4216        assert_eq!(ApplyPageType::from_str("ODD").unwrap(), ApplyPageType::Odd);
4217        assert!(ApplyPageType::from_str("invalid").is_err());
4218    }
4219
4220    #[test]
4221    fn apply_page_type_try_from_u8() {
4222        assert_eq!(ApplyPageType::try_from(0u8).unwrap(), ApplyPageType::Both);
4223        assert_eq!(ApplyPageType::try_from(1u8).unwrap(), ApplyPageType::Even);
4224        assert_eq!(ApplyPageType::try_from(2u8).unwrap(), ApplyPageType::Odd);
4225        assert!(ApplyPageType::try_from(3u8).is_err());
4226    }
4227
4228    #[test]
4229    fn apply_page_type_str_roundtrip() {
4230        for v in &[ApplyPageType::Both, ApplyPageType::Even, ApplyPageType::Odd] {
4231            let s = v.to_string();
4232            let back = ApplyPageType::from_str(&s).unwrap();
4233            assert_eq!(&back, v);
4234        }
4235    }
4236
4237    // ===================================================================
4238    // NumberFormatType
4239    // ===================================================================
4240
4241    #[test]
4242    fn number_format_type_default_is_digit() {
4243        assert_eq!(NumberFormatType::default(), NumberFormatType::Digit);
4244    }
4245
4246    #[test]
4247    fn number_format_type_display() {
4248        assert_eq!(NumberFormatType::Digit.to_string(), "Digit");
4249        assert_eq!(NumberFormatType::CircledDigit.to_string(), "CircledDigit");
4250        assert_eq!(NumberFormatType::RomanCapital.to_string(), "RomanCapital");
4251        assert_eq!(NumberFormatType::HanjaDigit.to_string(), "HanjaDigit");
4252    }
4253
4254    #[test]
4255    fn number_format_type_from_str() {
4256        assert_eq!(NumberFormatType::from_str("Digit").unwrap(), NumberFormatType::Digit);
4257        assert_eq!(NumberFormatType::from_str("DIGIT").unwrap(), NumberFormatType::Digit);
4258        assert_eq!(
4259            NumberFormatType::from_str("CircledDigit").unwrap(),
4260            NumberFormatType::CircledDigit
4261        );
4262        assert_eq!(
4263            NumberFormatType::from_str("ROMAN_CAPITAL").unwrap(),
4264            NumberFormatType::RomanCapital
4265        );
4266        assert!(NumberFormatType::from_str("invalid").is_err());
4267    }
4268
4269    #[test]
4270    fn number_format_type_try_from_u8() {
4271        assert_eq!(NumberFormatType::try_from(0u8).unwrap(), NumberFormatType::Digit);
4272        assert_eq!(NumberFormatType::try_from(1u8).unwrap(), NumberFormatType::CircledDigit);
4273        assert_eq!(NumberFormatType::try_from(8u8).unwrap(), NumberFormatType::HanjaDigit);
4274        assert_eq!(
4275            NumberFormatType::try_from(9u8).unwrap(),
4276            NumberFormatType::CircledHangulSyllable
4277        );
4278        assert_eq!(NumberFormatType::try_from(10u8).unwrap(), NumberFormatType::CircledLatinSmall);
4279        assert!(NumberFormatType::try_from(11u8).is_err());
4280    }
4281
4282    #[test]
4283    fn number_format_type_circled_hangul_syllable() {
4284        assert_eq!(NumberFormatType::CircledHangulSyllable.to_string(), "CircledHangulSyllable");
4285        assert_eq!(
4286            NumberFormatType::from_str("CircledHangulSyllable").unwrap(),
4287            NumberFormatType::CircledHangulSyllable
4288        );
4289        assert_eq!(
4290            NumberFormatType::from_str("CIRCLED_HANGUL_SYLLABLE").unwrap(),
4291            NumberFormatType::CircledHangulSyllable
4292        );
4293    }
4294
4295    #[test]
4296    fn number_format_type_str_roundtrip() {
4297        for v in &[
4298            NumberFormatType::Digit,
4299            NumberFormatType::CircledDigit,
4300            NumberFormatType::RomanCapital,
4301            NumberFormatType::RomanSmall,
4302            NumberFormatType::LatinCapital,
4303            NumberFormatType::LatinSmall,
4304            NumberFormatType::HangulSyllable,
4305            NumberFormatType::HangulJamo,
4306            NumberFormatType::HanjaDigit,
4307            NumberFormatType::CircledHangulSyllable,
4308            NumberFormatType::CircledLatinSmall,
4309        ] {
4310            let s = v.to_string();
4311            let back = NumberFormatType::from_str(&s).unwrap();
4312            assert_eq!(&back, v);
4313        }
4314    }
4315
4316    // ===================================================================
4317    // PageNumberPosition
4318    // ===================================================================
4319
4320    #[test]
4321    fn page_number_position_default_is_top_center() {
4322        assert_eq!(PageNumberPosition::default(), PageNumberPosition::TopCenter);
4323    }
4324
4325    #[test]
4326    fn page_number_position_display() {
4327        assert_eq!(PageNumberPosition::None.to_string(), "None");
4328        assert_eq!(PageNumberPosition::TopCenter.to_string(), "TopCenter");
4329        assert_eq!(PageNumberPosition::BottomCenter.to_string(), "BottomCenter");
4330        assert_eq!(PageNumberPosition::InsideBottom.to_string(), "InsideBottom");
4331    }
4332
4333    #[test]
4334    fn page_number_position_from_str() {
4335        assert_eq!(PageNumberPosition::from_str("None").unwrap(), PageNumberPosition::None);
4336        assert_eq!(
4337            PageNumberPosition::from_str("BOTTOM_CENTER").unwrap(),
4338            PageNumberPosition::BottomCenter
4339        );
4340        assert_eq!(
4341            PageNumberPosition::from_str("bottom-center").unwrap(),
4342            PageNumberPosition::BottomCenter
4343        );
4344        assert_eq!(PageNumberPosition::from_str("TopLeft").unwrap(), PageNumberPosition::TopLeft);
4345        assert!(PageNumberPosition::from_str("invalid").is_err());
4346    }
4347
4348    #[test]
4349    fn page_number_position_try_from_u8() {
4350        assert_eq!(PageNumberPosition::try_from(0u8).unwrap(), PageNumberPosition::None);
4351        assert_eq!(PageNumberPosition::try_from(2u8).unwrap(), PageNumberPosition::TopCenter);
4352        assert_eq!(PageNumberPosition::try_from(5u8).unwrap(), PageNumberPosition::BottomCenter);
4353        assert_eq!(PageNumberPosition::try_from(10u8).unwrap(), PageNumberPosition::InsideBottom);
4354        assert!(PageNumberPosition::try_from(11u8).is_err());
4355    }
4356
4357    #[test]
4358    fn page_number_position_str_roundtrip() {
4359        for v in &[
4360            PageNumberPosition::None,
4361            PageNumberPosition::TopLeft,
4362            PageNumberPosition::TopCenter,
4363            PageNumberPosition::TopRight,
4364            PageNumberPosition::BottomLeft,
4365            PageNumberPosition::BottomCenter,
4366            PageNumberPosition::BottomRight,
4367            PageNumberPosition::OutsideTop,
4368            PageNumberPosition::OutsideBottom,
4369            PageNumberPosition::InsideTop,
4370            PageNumberPosition::InsideBottom,
4371        ] {
4372            let s = v.to_string();
4373            let back = PageNumberPosition::from_str(&s).unwrap();
4374            assert_eq!(&back, v);
4375        }
4376    }
4377
4378    // ===================================================================
4379    // WordBreakType
4380    // ===================================================================
4381
4382    #[test]
4383    fn word_break_type_default_is_keep_word() {
4384        assert_eq!(WordBreakType::default(), WordBreakType::KeepWord);
4385    }
4386
4387    #[test]
4388    fn word_break_type_display() {
4389        assert_eq!(WordBreakType::KeepWord.to_string(), "KEEP_WORD");
4390        assert_eq!(WordBreakType::BreakWord.to_string(), "BREAK_WORD");
4391    }
4392
4393    #[test]
4394    fn word_break_type_from_str() {
4395        assert_eq!(WordBreakType::from_str("KEEP_WORD").unwrap(), WordBreakType::KeepWord);
4396        assert_eq!(WordBreakType::from_str("KeepWord").unwrap(), WordBreakType::KeepWord);
4397        assert_eq!(WordBreakType::from_str("keep_word").unwrap(), WordBreakType::KeepWord);
4398        assert_eq!(WordBreakType::from_str("BREAK_WORD").unwrap(), WordBreakType::BreakWord);
4399        assert_eq!(WordBreakType::from_str("BreakWord").unwrap(), WordBreakType::BreakWord);
4400        assert_eq!(WordBreakType::from_str("break_word").unwrap(), WordBreakType::BreakWord);
4401        assert!(WordBreakType::from_str("invalid").is_err());
4402    }
4403
4404    #[test]
4405    fn word_break_type_try_from_u8() {
4406        assert_eq!(WordBreakType::try_from(0u8).unwrap(), WordBreakType::KeepWord);
4407        assert_eq!(WordBreakType::try_from(1u8).unwrap(), WordBreakType::BreakWord);
4408        assert!(WordBreakType::try_from(2u8).is_err());
4409    }
4410
4411    #[test]
4412    fn word_break_type_serde_roundtrip() {
4413        for v in &[WordBreakType::KeepWord, WordBreakType::BreakWord] {
4414            let json = serde_json::to_string(v).unwrap();
4415            let back: WordBreakType = serde_json::from_str(&json).unwrap();
4416            assert_eq!(&back, v);
4417        }
4418    }
4419
4420    #[test]
4421    fn word_break_type_str_roundtrip() {
4422        for v in &[WordBreakType::KeepWord, WordBreakType::BreakWord] {
4423            let s = v.to_string();
4424            let back = WordBreakType::from_str(&s).unwrap();
4425            assert_eq!(&back, v);
4426        }
4427    }
4428
4429    // ===================================================================
4430    // EmphasisType
4431    // ===================================================================
4432
4433    #[test]
4434    fn emphasis_type_default_is_none() {
4435        assert_eq!(EmphasisType::default(), EmphasisType::None);
4436    }
4437
4438    #[test]
4439    fn emphasis_type_display_pascal_case() {
4440        assert_eq!(EmphasisType::None.to_string(), "None");
4441        assert_eq!(EmphasisType::DotAbove.to_string(), "DotAbove");
4442        assert_eq!(EmphasisType::RingAbove.to_string(), "RingAbove");
4443        assert_eq!(EmphasisType::Tilde.to_string(), "Tilde");
4444        assert_eq!(EmphasisType::Caron.to_string(), "Caron");
4445        assert_eq!(EmphasisType::Side.to_string(), "Side");
4446        assert_eq!(EmphasisType::Colon.to_string(), "Colon");
4447        assert_eq!(EmphasisType::GraveAccent.to_string(), "GraveAccent");
4448        assert_eq!(EmphasisType::AcuteAccent.to_string(), "AcuteAccent");
4449        assert_eq!(EmphasisType::Circumflex.to_string(), "Circumflex");
4450        assert_eq!(EmphasisType::Macron.to_string(), "Macron");
4451        assert_eq!(EmphasisType::HookAbove.to_string(), "HookAbove");
4452        assert_eq!(EmphasisType::DotBelow.to_string(), "DotBelow");
4453    }
4454
4455    #[test]
4456    fn emphasis_type_from_str_screaming_snake_case() {
4457        assert_eq!(EmphasisType::from_str("NONE").unwrap(), EmphasisType::None);
4458        assert_eq!(EmphasisType::from_str("DOT_ABOVE").unwrap(), EmphasisType::DotAbove);
4459        assert_eq!(EmphasisType::from_str("RING_ABOVE").unwrap(), EmphasisType::RingAbove);
4460        assert_eq!(EmphasisType::from_str("GRAVE_ACCENT").unwrap(), EmphasisType::GraveAccent);
4461        assert_eq!(EmphasisType::from_str("DOT_BELOW").unwrap(), EmphasisType::DotBelow);
4462    }
4463
4464    #[test]
4465    fn emphasis_type_from_str_pascal_case() {
4466        assert_eq!(EmphasisType::from_str("None").unwrap(), EmphasisType::None);
4467        assert_eq!(EmphasisType::from_str("DotAbove").unwrap(), EmphasisType::DotAbove);
4468        assert_eq!(EmphasisType::from_str("HookAbove").unwrap(), EmphasisType::HookAbove);
4469    }
4470
4471    #[test]
4472    fn emphasis_type_from_str_invalid() {
4473        let err = EmphasisType::from_str("INVALID").unwrap_err();
4474        match err {
4475            FoundationError::ParseError { ref type_name, ref value, .. } => {
4476                assert_eq!(type_name, "EmphasisType");
4477                assert_eq!(value, "INVALID");
4478            }
4479            other => panic!("unexpected: {other}"),
4480        }
4481    }
4482
4483    #[test]
4484    fn emphasis_type_try_from_u8() {
4485        assert_eq!(EmphasisType::try_from(0u8).unwrap(), EmphasisType::None);
4486        assert_eq!(EmphasisType::try_from(1u8).unwrap(), EmphasisType::DotAbove);
4487        assert_eq!(EmphasisType::try_from(12u8).unwrap(), EmphasisType::DotBelow);
4488        assert!(EmphasisType::try_from(13u8).is_err());
4489        assert!(EmphasisType::try_from(255u8).is_err());
4490    }
4491
4492    #[test]
4493    fn emphasis_type_repr_values() {
4494        assert_eq!(EmphasisType::None as u8, 0);
4495        assert_eq!(EmphasisType::DotAbove as u8, 1);
4496        assert_eq!(EmphasisType::DotBelow as u8, 12);
4497    }
4498
4499    #[test]
4500    fn emphasis_type_serde_roundtrip() {
4501        for variant in &[
4502            EmphasisType::None,
4503            EmphasisType::DotAbove,
4504            EmphasisType::RingAbove,
4505            EmphasisType::DotBelow,
4506        ] {
4507            let json = serde_json::to_string(variant).unwrap();
4508            let back: EmphasisType = serde_json::from_str(&json).unwrap();
4509            assert_eq!(&back, variant);
4510        }
4511    }
4512
4513    #[test]
4514    fn emphasis_type_str_roundtrip() {
4515        for variant in &[
4516            EmphasisType::None,
4517            EmphasisType::DotAbove,
4518            EmphasisType::GraveAccent,
4519            EmphasisType::DotBelow,
4520        ] {
4521            let s = variant.to_string();
4522            let back = EmphasisType::from_str(&s).unwrap();
4523            assert_eq!(&back, variant);
4524        }
4525    }
4526}