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}
1289
1290impl fmt::Display for NumberFormatType {
1291    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1292        match self {
1293            Self::Digit => f.write_str("Digit"),
1294            Self::CircledDigit => f.write_str("CircledDigit"),
1295            Self::RomanCapital => f.write_str("RomanCapital"),
1296            Self::RomanSmall => f.write_str("RomanSmall"),
1297            Self::LatinCapital => f.write_str("LatinCapital"),
1298            Self::LatinSmall => f.write_str("LatinSmall"),
1299            Self::HangulSyllable => f.write_str("HangulSyllable"),
1300            Self::HangulJamo => f.write_str("HangulJamo"),
1301            Self::HanjaDigit => f.write_str("HanjaDigit"),
1302            Self::CircledHangulSyllable => f.write_str("CircledHangulSyllable"),
1303        }
1304    }
1305}
1306
1307impl std::str::FromStr for NumberFormatType {
1308    type Err = FoundationError;
1309
1310    fn from_str(s: &str) -> Result<Self, Self::Err> {
1311        match s {
1312            "Digit" | "digit" | "DIGIT" => Ok(Self::Digit),
1313            "CircledDigit" | "circleddigit" | "CIRCLED_DIGIT" => Ok(Self::CircledDigit),
1314            "RomanCapital" | "romancapital" | "ROMAN_CAPITAL" => Ok(Self::RomanCapital),
1315            "RomanSmall" | "romansmall" | "ROMAN_SMALL" => Ok(Self::RomanSmall),
1316            "LatinCapital" | "latincapital" | "LATIN_CAPITAL" => Ok(Self::LatinCapital),
1317            "LatinSmall" | "latinsmall" | "LATIN_SMALL" => Ok(Self::LatinSmall),
1318            "HangulSyllable" | "hangulsyllable" | "HANGUL_SYLLABLE" => Ok(Self::HangulSyllable),
1319            "HangulJamo" | "hanguljamo" | "HANGUL_JAMO" => Ok(Self::HangulJamo),
1320            "HanjaDigit" | "hanjadigit" | "HANJA_DIGIT" => Ok(Self::HanjaDigit),
1321            "CircledHangulSyllable" | "circledhangulsyllable" | "CIRCLED_HANGUL_SYLLABLE" => {
1322                Ok(Self::CircledHangulSyllable)
1323            }
1324            _ => Err(FoundationError::ParseError {
1325                type_name: "NumberFormatType".to_string(),
1326                value: s.to_string(),
1327                valid_values: "Digit, CircledDigit, RomanCapital, RomanSmall, LatinCapital, LatinSmall, HangulSyllable, HangulJamo, HanjaDigit, CircledHangulSyllable".to_string(),
1328            }),
1329        }
1330    }
1331}
1332
1333impl TryFrom<u8> for NumberFormatType {
1334    type Error = FoundationError;
1335
1336    fn try_from(value: u8) -> Result<Self, Self::Error> {
1337        match value {
1338            0 => Ok(Self::Digit),
1339            1 => Ok(Self::CircledDigit),
1340            2 => Ok(Self::RomanCapital),
1341            3 => Ok(Self::RomanSmall),
1342            4 => Ok(Self::LatinCapital),
1343            5 => Ok(Self::LatinSmall),
1344            6 => Ok(Self::HangulSyllable),
1345            7 => Ok(Self::HangulJamo),
1346            8 => Ok(Self::HanjaDigit),
1347            9 => Ok(Self::CircledHangulSyllable),
1348            _ => Err(FoundationError::ParseError {
1349                type_name: "NumberFormatType".to_string(),
1350                value: value.to_string(),
1351                valid_values: "0-9 (Digit, CircledDigit, RomanCapital, RomanSmall, LatinCapital, LatinSmall, HangulSyllable, HangulJamo, HanjaDigit, CircledHangulSyllable)".to_string(),
1352            }),
1353        }
1354    }
1355}
1356
1357impl schemars::JsonSchema for NumberFormatType {
1358    fn schema_name() -> std::borrow::Cow<'static, str> {
1359        std::borrow::Cow::Borrowed("NumberFormatType")
1360    }
1361
1362    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1363        gen.subschema_for::<String>()
1364    }
1365}
1366
1367// ---------------------------------------------------------------------------
1368// PageNumberPosition
1369// ---------------------------------------------------------------------------
1370
1371/// Position of page numbers on the page.
1372///
1373/// # Examples
1374///
1375/// ```
1376/// use hwpforge_foundation::PageNumberPosition;
1377///
1378/// assert_eq!(PageNumberPosition::default(), PageNumberPosition::TopCenter);
1379/// ```
1380#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1381#[non_exhaustive]
1382#[repr(u8)]
1383pub enum PageNumberPosition {
1384    /// No page number.
1385    None = 0,
1386    /// Top left.
1387    TopLeft = 1,
1388    /// Top center (default).
1389    #[default]
1390    TopCenter = 2,
1391    /// Top right.
1392    TopRight = 3,
1393    /// Bottom left.
1394    BottomLeft = 4,
1395    /// Bottom center.
1396    BottomCenter = 5,
1397    /// Bottom right.
1398    BottomRight = 6,
1399    /// Outside top.
1400    OutsideTop = 7,
1401    /// Outside bottom.
1402    OutsideBottom = 8,
1403    /// Inside top.
1404    InsideTop = 9,
1405    /// Inside bottom.
1406    InsideBottom = 10,
1407}
1408
1409impl fmt::Display for PageNumberPosition {
1410    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1411        match self {
1412            Self::None => f.write_str("None"),
1413            Self::TopLeft => f.write_str("TopLeft"),
1414            Self::TopCenter => f.write_str("TopCenter"),
1415            Self::TopRight => f.write_str("TopRight"),
1416            Self::BottomLeft => f.write_str("BottomLeft"),
1417            Self::BottomCenter => f.write_str("BottomCenter"),
1418            Self::BottomRight => f.write_str("BottomRight"),
1419            Self::OutsideTop => f.write_str("OutsideTop"),
1420            Self::OutsideBottom => f.write_str("OutsideBottom"),
1421            Self::InsideTop => f.write_str("InsideTop"),
1422            Self::InsideBottom => f.write_str("InsideBottom"),
1423        }
1424    }
1425}
1426
1427impl std::str::FromStr for PageNumberPosition {
1428    type Err = FoundationError;
1429
1430    fn from_str(s: &str) -> Result<Self, Self::Err> {
1431        match s {
1432            "None" | "none" | "NONE" => Ok(Self::None),
1433            "TopLeft" | "topleft" | "TOP_LEFT" | "top-left" => Ok(Self::TopLeft),
1434            "TopCenter" | "topcenter" | "TOP_CENTER" | "top-center" => Ok(Self::TopCenter),
1435            "TopRight" | "topright" | "TOP_RIGHT" | "top-right" => Ok(Self::TopRight),
1436            "BottomLeft" | "bottomleft" | "BOTTOM_LEFT" | "bottom-left" => Ok(Self::BottomLeft),
1437            "BottomCenter" | "bottomcenter" | "BOTTOM_CENTER" | "bottom-center" => {
1438                Ok(Self::BottomCenter)
1439            }
1440            "BottomRight" | "bottomright" | "BOTTOM_RIGHT" | "bottom-right" => {
1441                Ok(Self::BottomRight)
1442            }
1443            "OutsideTop" | "outsidetop" | "OUTSIDE_TOP" | "outside-top" => Ok(Self::OutsideTop),
1444            "OutsideBottom" | "outsidebottom" | "OUTSIDE_BOTTOM" | "outside-bottom" => {
1445                Ok(Self::OutsideBottom)
1446            }
1447            "InsideTop" | "insidetop" | "INSIDE_TOP" | "inside-top" => Ok(Self::InsideTop),
1448            "InsideBottom" | "insidebottom" | "INSIDE_BOTTOM" | "inside-bottom" => {
1449                Ok(Self::InsideBottom)
1450            }
1451            _ => Err(FoundationError::ParseError {
1452                type_name: "PageNumberPosition".to_string(),
1453                value: s.to_string(),
1454                valid_values: "None, TopLeft, TopCenter, TopRight, BottomLeft, BottomCenter, BottomRight, OutsideTop, OutsideBottom, InsideTop, InsideBottom".to_string(),
1455            }),
1456        }
1457    }
1458}
1459
1460impl TryFrom<u8> for PageNumberPosition {
1461    type Error = FoundationError;
1462
1463    fn try_from(value: u8) -> Result<Self, Self::Error> {
1464        match value {
1465            0 => Ok(Self::None),
1466            1 => Ok(Self::TopLeft),
1467            2 => Ok(Self::TopCenter),
1468            3 => Ok(Self::TopRight),
1469            4 => Ok(Self::BottomLeft),
1470            5 => Ok(Self::BottomCenter),
1471            6 => Ok(Self::BottomRight),
1472            7 => Ok(Self::OutsideTop),
1473            8 => Ok(Self::OutsideBottom),
1474            9 => Ok(Self::InsideTop),
1475            10 => Ok(Self::InsideBottom),
1476            _ => Err(FoundationError::ParseError {
1477                type_name: "PageNumberPosition".to_string(),
1478                value: value.to_string(),
1479                valid_values: "0-10 (None, TopLeft, TopCenter, TopRight, BottomLeft, BottomCenter, BottomRight, OutsideTop, OutsideBottom, InsideTop, InsideBottom)".to_string(),
1480            }),
1481        }
1482    }
1483}
1484
1485impl schemars::JsonSchema for PageNumberPosition {
1486    fn schema_name() -> std::borrow::Cow<'static, str> {
1487        std::borrow::Cow::Borrowed("PageNumberPosition")
1488    }
1489
1490    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1491        gen.subschema_for::<String>()
1492    }
1493}
1494
1495// ---------------------------------------------------------------------------
1496// WordBreakType
1497// ---------------------------------------------------------------------------
1498
1499/// Word-breaking behavior for paragraph text justification.
1500///
1501/// Controls how 한글 distributes extra space in justified text.
1502/// `KeepWord` preserves word boundaries (natural spacing),
1503/// `BreakWord` allows breaking at any character (stretched spacing).
1504///
1505/// # Examples
1506///
1507/// ```
1508/// use hwpforge_foundation::WordBreakType;
1509///
1510/// assert_eq!(WordBreakType::default(), WordBreakType::KeepWord);
1511/// assert_eq!(WordBreakType::KeepWord.to_string(), "KEEP_WORD");
1512/// ```
1513#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1514#[non_exhaustive]
1515#[repr(u8)]
1516pub enum WordBreakType {
1517    /// Keep words intact — distribute space between words only (한글 default).
1518    #[default]
1519    KeepWord = 0,
1520    /// Allow breaking at any character — distribute space between all characters.
1521    BreakWord = 1,
1522}
1523
1524impl fmt::Display for WordBreakType {
1525    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1526        match self {
1527            Self::KeepWord => f.write_str("KEEP_WORD"),
1528            Self::BreakWord => f.write_str("BREAK_WORD"),
1529        }
1530    }
1531}
1532
1533impl std::str::FromStr for WordBreakType {
1534    type Err = FoundationError;
1535
1536    fn from_str(s: &str) -> Result<Self, Self::Err> {
1537        match s {
1538            "KEEP_WORD" | "KeepWord" | "keep_word" => Ok(Self::KeepWord),
1539            "BREAK_WORD" | "BreakWord" | "break_word" => Ok(Self::BreakWord),
1540            _ => Err(FoundationError::ParseError {
1541                type_name: "WordBreakType".to_string(),
1542                value: s.to_string(),
1543                valid_values: "KEEP_WORD, BREAK_WORD".to_string(),
1544            }),
1545        }
1546    }
1547}
1548
1549impl TryFrom<u8> for WordBreakType {
1550    type Error = FoundationError;
1551
1552    fn try_from(value: u8) -> Result<Self, Self::Error> {
1553        match value {
1554            0 => Ok(Self::KeepWord),
1555            1 => Ok(Self::BreakWord),
1556            _ => Err(FoundationError::ParseError {
1557                type_name: "WordBreakType".to_string(),
1558                value: value.to_string(),
1559                valid_values: "0 (KeepWord), 1 (BreakWord)".to_string(),
1560            }),
1561        }
1562    }
1563}
1564
1565impl schemars::JsonSchema for WordBreakType {
1566    fn schema_name() -> std::borrow::Cow<'static, str> {
1567        std::borrow::Cow::Borrowed("WordBreakType")
1568    }
1569
1570    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1571        gen.subschema_for::<String>()
1572    }
1573}
1574
1575// ---------------------------------------------------------------------------
1576// EmphasisType
1577// ---------------------------------------------------------------------------
1578
1579/// Character emphasis mark (symMark attribute in HWPX).
1580///
1581/// Controls the emphasis symbol displayed above or below characters.
1582/// Maps to HWPX `symMark` attribute values.
1583///
1584/// # Examples
1585///
1586/// ```
1587/// use hwpforge_foundation::EmphasisType;
1588///
1589/// assert_eq!(EmphasisType::default(), EmphasisType::None);
1590/// ```
1591#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1592#[non_exhaustive]
1593#[repr(u8)]
1594pub enum EmphasisType {
1595    /// No emphasis mark (default).
1596    #[default]
1597    None = 0,
1598    /// Dot above character.
1599    DotAbove = 1,
1600    /// Ring above character.
1601    RingAbove = 2,
1602    /// Tilde above character.
1603    Tilde = 3,
1604    /// Caron (hacek) above character.
1605    Caron = 4,
1606    /// Side dot.
1607    Side = 5,
1608    /// Colon mark.
1609    Colon = 6,
1610    /// Grave accent.
1611    GraveAccent = 7,
1612    /// Acute accent.
1613    AcuteAccent = 8,
1614    /// Circumflex accent.
1615    Circumflex = 9,
1616    /// Macron (overline).
1617    Macron = 10,
1618    /// Hook above.
1619    HookAbove = 11,
1620    /// Dot below character.
1621    DotBelow = 12,
1622}
1623
1624impl fmt::Display for EmphasisType {
1625    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1626        match self {
1627            Self::None => f.write_str("None"),
1628            Self::DotAbove => f.write_str("DotAbove"),
1629            Self::RingAbove => f.write_str("RingAbove"),
1630            Self::Tilde => f.write_str("Tilde"),
1631            Self::Caron => f.write_str("Caron"),
1632            Self::Side => f.write_str("Side"),
1633            Self::Colon => f.write_str("Colon"),
1634            Self::GraveAccent => f.write_str("GraveAccent"),
1635            Self::AcuteAccent => f.write_str("AcuteAccent"),
1636            Self::Circumflex => f.write_str("Circumflex"),
1637            Self::Macron => f.write_str("Macron"),
1638            Self::HookAbove => f.write_str("HookAbove"),
1639            Self::DotBelow => f.write_str("DotBelow"),
1640        }
1641    }
1642}
1643
1644impl std::str::FromStr for EmphasisType {
1645    type Err = FoundationError;
1646
1647    fn from_str(s: &str) -> Result<Self, Self::Err> {
1648        match s {
1649            "NONE" | "None" | "none" => Ok(Self::None),
1650            "DOT_ABOVE" | "DotAbove" | "dot_above" => Ok(Self::DotAbove),
1651            "RING_ABOVE" | "RingAbove" | "ring_above" => Ok(Self::RingAbove),
1652            "TILDE" | "Tilde" | "tilde" => Ok(Self::Tilde),
1653            "CARON" | "Caron" | "caron" => Ok(Self::Caron),
1654            "SIDE" | "Side" | "side" => Ok(Self::Side),
1655            "COLON" | "Colon" | "colon" => Ok(Self::Colon),
1656            "GRAVE_ACCENT" | "GraveAccent" | "grave_accent" => Ok(Self::GraveAccent),
1657            "ACUTE_ACCENT" | "AcuteAccent" | "acute_accent" => Ok(Self::AcuteAccent),
1658            "CIRCUMFLEX" | "Circumflex" | "circumflex" => Ok(Self::Circumflex),
1659            "MACRON" | "Macron" | "macron" => Ok(Self::Macron),
1660            "HOOK_ABOVE" | "HookAbove" | "hook_above" => Ok(Self::HookAbove),
1661            "DOT_BELOW" | "DotBelow" | "dot_below" => Ok(Self::DotBelow),
1662            _ => Err(FoundationError::ParseError {
1663                type_name: "EmphasisType".to_string(),
1664                value: s.to_string(),
1665                valid_values:
1666                    "NONE, DOT_ABOVE, RING_ABOVE, TILDE, CARON, SIDE, COLON, GRAVE_ACCENT, ACUTE_ACCENT, CIRCUMFLEX, MACRON, HOOK_ABOVE, DOT_BELOW"
1667                        .to_string(),
1668            }),
1669        }
1670    }
1671}
1672
1673impl TryFrom<u8> for EmphasisType {
1674    type Error = FoundationError;
1675
1676    fn try_from(value: u8) -> Result<Self, Self::Error> {
1677        match value {
1678            0 => Ok(Self::None),
1679            1 => Ok(Self::DotAbove),
1680            2 => Ok(Self::RingAbove),
1681            3 => Ok(Self::Tilde),
1682            4 => Ok(Self::Caron),
1683            5 => Ok(Self::Side),
1684            6 => Ok(Self::Colon),
1685            7 => Ok(Self::GraveAccent),
1686            8 => Ok(Self::AcuteAccent),
1687            9 => Ok(Self::Circumflex),
1688            10 => Ok(Self::Macron),
1689            11 => Ok(Self::HookAbove),
1690            12 => Ok(Self::DotBelow),
1691            _ => Err(FoundationError::ParseError {
1692                type_name: "EmphasisType".to_string(),
1693                value: value.to_string(),
1694                valid_values: "0-12 (None through DotBelow)".to_string(),
1695            }),
1696        }
1697    }
1698}
1699
1700impl schemars::JsonSchema for EmphasisType {
1701    fn schema_name() -> std::borrow::Cow<'static, str> {
1702        std::borrow::Cow::Borrowed("EmphasisType")
1703    }
1704
1705    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1706        gen.subschema_for::<String>()
1707    }
1708}
1709
1710// ---------------------------------------------------------------------------
1711// HeadingType
1712// ---------------------------------------------------------------------------
1713
1714/// Paragraph heading type for outline/numbering classification.
1715///
1716/// Controls how a paragraph participates in document outline or numbering.
1717/// Maps to the HWPX `<hh:heading type="...">` attribute.
1718///
1719/// # Examples
1720///
1721/// ```
1722/// use hwpforge_foundation::HeadingType;
1723///
1724/// assert_eq!(HeadingType::default(), HeadingType::None);
1725/// ```
1726#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1727#[non_exhaustive]
1728#[repr(u8)]
1729pub enum HeadingType {
1730    /// No heading (body text, default).
1731    #[default]
1732    None = 0,
1733    /// Outline heading (개요).
1734    Outline = 1,
1735    /// Number heading.
1736    Number = 2,
1737    /// Bullet heading.
1738    Bullet = 3,
1739}
1740
1741impl HeadingType {
1742    /// Converts to the HWPX XML attribute string.
1743    pub fn to_hwpx_str(self) -> &'static str {
1744        match self {
1745            Self::None => "NONE",
1746            Self::Outline => "OUTLINE",
1747            Self::Number => "NUMBER",
1748            Self::Bullet => "BULLET",
1749        }
1750    }
1751
1752    /// Parses a HWPX XML attribute string.
1753    pub fn from_hwpx_str(s: &str) -> Self {
1754        match s {
1755            "NONE" => Self::None,
1756            "OUTLINE" => Self::Outline,
1757            "NUMBER" => Self::Number,
1758            "BULLET" => Self::Bullet,
1759            _ => Self::None,
1760        }
1761    }
1762}
1763
1764impl fmt::Display for HeadingType {
1765    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1766        match self {
1767            Self::None => f.write_str("None"),
1768            Self::Outline => f.write_str("Outline"),
1769            Self::Number => f.write_str("Number"),
1770            Self::Bullet => f.write_str("Bullet"),
1771        }
1772    }
1773}
1774
1775impl std::str::FromStr for HeadingType {
1776    type Err = FoundationError;
1777
1778    fn from_str(s: &str) -> Result<Self, Self::Err> {
1779        match s {
1780            "None" | "none" | "NONE" => Ok(Self::None),
1781            "Outline" | "outline" | "OUTLINE" => Ok(Self::Outline),
1782            "Number" | "number" | "NUMBER" => Ok(Self::Number),
1783            "Bullet" | "bullet" | "BULLET" => Ok(Self::Bullet),
1784            _ => Err(FoundationError::ParseError {
1785                type_name: "HeadingType".to_string(),
1786                value: s.to_string(),
1787                valid_values: "None, Outline, Number, Bullet".to_string(),
1788            }),
1789        }
1790    }
1791}
1792
1793impl TryFrom<u8> for HeadingType {
1794    type Error = FoundationError;
1795
1796    fn try_from(value: u8) -> Result<Self, Self::Error> {
1797        match value {
1798            0 => Ok(Self::None),
1799            1 => Ok(Self::Outline),
1800            2 => Ok(Self::Number),
1801            3 => Ok(Self::Bullet),
1802            _ => Err(FoundationError::ParseError {
1803                type_name: "HeadingType".to_string(),
1804                value: value.to_string(),
1805                valid_values: "0 (None), 1 (Outline), 2 (Number), 3 (Bullet)".to_string(),
1806            }),
1807        }
1808    }
1809}
1810
1811impl schemars::JsonSchema for HeadingType {
1812    fn schema_name() -> std::borrow::Cow<'static, str> {
1813        std::borrow::Cow::Borrowed("HeadingType")
1814    }
1815
1816    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1817        gen.subschema_for::<String>()
1818    }
1819}
1820
1821// ---------------------------------------------------------------------------
1822// TabAlign
1823// ---------------------------------------------------------------------------
1824
1825/// Tab stop alignment.
1826///
1827/// Maps to HWPX `<hh:tabItem type="...">`.
1828#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1829#[non_exhaustive]
1830#[repr(u8)]
1831pub enum TabAlign {
1832    /// Left-aligned tab.
1833    #[default]
1834    Left = 0,
1835    /// Right-aligned tab.
1836    Right = 1,
1837    /// Center-aligned tab.
1838    Center = 2,
1839    /// Decimal-aligned tab.
1840    Decimal = 3,
1841}
1842
1843impl TabAlign {
1844    /// Converts to the HWPX XML attribute string.
1845    pub fn to_hwpx_str(self) -> &'static str {
1846        match self {
1847            Self::Left => "LEFT",
1848            Self::Right => "RIGHT",
1849            Self::Center => "CENTER",
1850            Self::Decimal => "DECIMAL",
1851        }
1852    }
1853
1854    /// Parses a HWPX XML attribute string.
1855    pub fn from_hwpx_str(s: &str) -> Self {
1856        match s {
1857            "RIGHT" => Self::Right,
1858            "CENTER" => Self::Center,
1859            "DECIMAL" => Self::Decimal,
1860            _ => Self::Left,
1861        }
1862    }
1863}
1864
1865impl fmt::Display for TabAlign {
1866    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1867        match self {
1868            Self::Left => f.write_str("Left"),
1869            Self::Right => f.write_str("Right"),
1870            Self::Center => f.write_str("Center"),
1871            Self::Decimal => f.write_str("Decimal"),
1872        }
1873    }
1874}
1875
1876impl std::str::FromStr for TabAlign {
1877    type Err = FoundationError;
1878
1879    fn from_str(s: &str) -> Result<Self, Self::Err> {
1880        match s {
1881            "Left" | "LEFT" | "left" => Ok(Self::Left),
1882            "Right" | "RIGHT" | "right" => Ok(Self::Right),
1883            "Center" | "CENTER" | "center" => Ok(Self::Center),
1884            "Decimal" | "DECIMAL" | "decimal" => Ok(Self::Decimal),
1885            _ => Err(FoundationError::ParseError {
1886                type_name: "TabAlign".to_string(),
1887                value: s.to_string(),
1888                valid_values: "Left, Right, Center, Decimal".to_string(),
1889            }),
1890        }
1891    }
1892}
1893
1894impl schemars::JsonSchema for TabAlign {
1895    fn schema_name() -> std::borrow::Cow<'static, str> {
1896        std::borrow::Cow::Borrowed("TabAlign")
1897    }
1898
1899    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1900        gen.subschema_for::<String>()
1901    }
1902}
1903
1904// ---------------------------------------------------------------------------
1905// TabLeader
1906// ---------------------------------------------------------------------------
1907
1908/// Tab leader line style.
1909///
1910/// Stored as an uppercase HWPX-compatible string so unknown vendor values
1911/// survive roundtrip instead of being silently flattened.
1912#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1913#[serde(transparent)]
1914pub struct TabLeader(String);
1915
1916impl TabLeader {
1917    /// Creates a leader from a HWPX line type string.
1918    pub fn from_hwpx_str(s: &str) -> Self {
1919        Self(s.to_ascii_uppercase())
1920    }
1921
1922    /// Returns the canonical HWPX string.
1923    pub fn as_hwpx_str(&self) -> &str {
1924        &self.0
1925    }
1926
1927    /// No leader.
1928    pub fn none() -> Self {
1929        Self::from_hwpx_str("NONE")
1930    }
1931
1932    /// Dotted leader.
1933    pub fn dot() -> Self {
1934        Self::from_hwpx_str("DOT")
1935    }
1936}
1937
1938impl fmt::Display for TabLeader {
1939    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1940        f.write_str(self.as_hwpx_str())
1941    }
1942}
1943
1944impl std::str::FromStr for TabLeader {
1945    type Err = FoundationError;
1946
1947    fn from_str(s: &str) -> Result<Self, Self::Err> {
1948        Ok(Self::from_hwpx_str(s))
1949    }
1950}
1951
1952impl schemars::JsonSchema for TabLeader {
1953    fn schema_name() -> std::borrow::Cow<'static, str> {
1954        std::borrow::Cow::Borrowed("TabLeader")
1955    }
1956
1957    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
1958        gen.subschema_for::<String>()
1959    }
1960}
1961
1962// ---------------------------------------------------------------------------
1963// GutterType
1964// ---------------------------------------------------------------------------
1965
1966/// Gutter position type for page margins.
1967///
1968/// Controls where the binding gutter space is placed on the page.
1969/// Used in `<hp:pagePr gutterType="...">`.
1970///
1971/// # Examples
1972///
1973/// ```
1974/// use hwpforge_foundation::GutterType;
1975///
1976/// assert_eq!(GutterType::default(), GutterType::LeftOnly);
1977/// assert_eq!(GutterType::LeftOnly.to_string(), "LeftOnly");
1978/// ```
1979#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1980#[non_exhaustive]
1981#[repr(u8)]
1982pub enum GutterType {
1983    /// Gutter on the left side only (default).
1984    #[default]
1985    LeftOnly = 0,
1986    /// Gutter on the left and right sides.
1987    LeftRight = 1,
1988    /// Gutter on the top side only.
1989    TopOnly = 2,
1990    /// Gutter on the top and bottom sides.
1991    TopBottom = 3,
1992}
1993
1994impl fmt::Display for GutterType {
1995    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1996        match self {
1997            Self::LeftOnly => f.write_str("LeftOnly"),
1998            Self::LeftRight => f.write_str("LeftRight"),
1999            Self::TopOnly => f.write_str("TopOnly"),
2000            Self::TopBottom => f.write_str("TopBottom"),
2001        }
2002    }
2003}
2004
2005impl std::str::FromStr for GutterType {
2006    type Err = FoundationError;
2007
2008    fn from_str(s: &str) -> Result<Self, Self::Err> {
2009        match s {
2010            "LeftOnly" | "LEFT_ONLY" | "left_only" => Ok(Self::LeftOnly),
2011            "LeftRight" | "LEFT_RIGHT" | "left_right" => Ok(Self::LeftRight),
2012            "TopOnly" | "TOP_ONLY" | "top_only" => Ok(Self::TopOnly),
2013            "TopBottom" | "TOP_BOTTOM" | "top_bottom" => Ok(Self::TopBottom),
2014            _ => Err(FoundationError::ParseError {
2015                type_name: "GutterType".to_string(),
2016                value: s.to_string(),
2017                valid_values: "LeftOnly, LeftRight, TopOnly, TopBottom".to_string(),
2018            }),
2019        }
2020    }
2021}
2022
2023impl TryFrom<u8> for GutterType {
2024    type Error = FoundationError;
2025
2026    fn try_from(value: u8) -> Result<Self, Self::Error> {
2027        match value {
2028            0 => Ok(Self::LeftOnly),
2029            1 => Ok(Self::LeftRight),
2030            2 => Ok(Self::TopOnly),
2031            3 => Ok(Self::TopBottom),
2032            _ => Err(FoundationError::ParseError {
2033                type_name: "GutterType".to_string(),
2034                value: value.to_string(),
2035                valid_values: "0 (LeftOnly), 1 (LeftRight), 2 (TopOnly), 3 (TopBottom)".to_string(),
2036            }),
2037        }
2038    }
2039}
2040
2041impl schemars::JsonSchema for GutterType {
2042    fn schema_name() -> std::borrow::Cow<'static, str> {
2043        std::borrow::Cow::Borrowed("GutterType")
2044    }
2045
2046    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2047        gen.subschema_for::<String>()
2048    }
2049}
2050
2051// ---------------------------------------------------------------------------
2052// ShowMode
2053// ---------------------------------------------------------------------------
2054
2055/// Visibility mode for page borders and fills.
2056///
2057/// Controls on which pages the border or fill is displayed.
2058/// Used in `<hp:visibility border="..." fill="...">`.
2059///
2060/// # Examples
2061///
2062/// ```
2063/// use hwpforge_foundation::ShowMode;
2064///
2065/// assert_eq!(ShowMode::default(), ShowMode::ShowAll);
2066/// assert_eq!(ShowMode::ShowAll.to_string(), "ShowAll");
2067/// ```
2068#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2069#[non_exhaustive]
2070#[repr(u8)]
2071pub enum ShowMode {
2072    /// Show on all pages (default).
2073    #[default]
2074    ShowAll = 0,
2075    /// Hide on all pages.
2076    HideAll = 1,
2077    /// Show on odd pages only.
2078    ShowOdd = 2,
2079    /// Show on even pages only.
2080    ShowEven = 3,
2081}
2082
2083impl fmt::Display for ShowMode {
2084    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2085        match self {
2086            Self::ShowAll => f.write_str("ShowAll"),
2087            Self::HideAll => f.write_str("HideAll"),
2088            Self::ShowOdd => f.write_str("ShowOdd"),
2089            Self::ShowEven => f.write_str("ShowEven"),
2090        }
2091    }
2092}
2093
2094impl std::str::FromStr for ShowMode {
2095    type Err = FoundationError;
2096
2097    fn from_str(s: &str) -> Result<Self, Self::Err> {
2098        match s {
2099            "ShowAll" | "SHOW_ALL" | "show_all" => Ok(Self::ShowAll),
2100            "HideAll" | "HIDE_ALL" | "hide_all" => Ok(Self::HideAll),
2101            "ShowOdd" | "SHOW_ODD" | "show_odd" => Ok(Self::ShowOdd),
2102            "ShowEven" | "SHOW_EVEN" | "show_even" => Ok(Self::ShowEven),
2103            _ => Err(FoundationError::ParseError {
2104                type_name: "ShowMode".to_string(),
2105                value: s.to_string(),
2106                valid_values: "ShowAll, HideAll, ShowOdd, ShowEven".to_string(),
2107            }),
2108        }
2109    }
2110}
2111
2112impl TryFrom<u8> for ShowMode {
2113    type Error = FoundationError;
2114
2115    fn try_from(value: u8) -> Result<Self, Self::Error> {
2116        match value {
2117            0 => Ok(Self::ShowAll),
2118            1 => Ok(Self::HideAll),
2119            2 => Ok(Self::ShowOdd),
2120            3 => Ok(Self::ShowEven),
2121            _ => Err(FoundationError::ParseError {
2122                type_name: "ShowMode".to_string(),
2123                value: value.to_string(),
2124                valid_values: "0 (ShowAll), 1 (HideAll), 2 (ShowOdd), 3 (ShowEven)".to_string(),
2125            }),
2126        }
2127    }
2128}
2129
2130impl schemars::JsonSchema for ShowMode {
2131    fn schema_name() -> std::borrow::Cow<'static, str> {
2132        std::borrow::Cow::Borrowed("ShowMode")
2133    }
2134
2135    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2136        gen.subschema_for::<String>()
2137    }
2138}
2139
2140// ---------------------------------------------------------------------------
2141// RestartType
2142// ---------------------------------------------------------------------------
2143
2144/// Line number restart type.
2145///
2146/// Controls when line numbering restarts to 1.
2147/// Used in `<hp:lineNumberShape restartType="...">`.
2148///
2149/// # Examples
2150///
2151/// ```
2152/// use hwpforge_foundation::RestartType;
2153///
2154/// assert_eq!(RestartType::default(), RestartType::Continuous);
2155/// assert_eq!(RestartType::Continuous.to_string(), "Continuous");
2156/// ```
2157#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2158#[non_exhaustive]
2159#[repr(u8)]
2160pub enum RestartType {
2161    /// Continuous numbering throughout the document (default).
2162    #[default]
2163    Continuous = 0,
2164    /// Restart numbering at each section.
2165    Section = 1,
2166    /// Restart numbering at each page.
2167    Page = 2,
2168}
2169
2170impl fmt::Display for RestartType {
2171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2172        match self {
2173            Self::Continuous => f.write_str("Continuous"),
2174            Self::Section => f.write_str("Section"),
2175            Self::Page => f.write_str("Page"),
2176        }
2177    }
2178}
2179
2180impl std::str::FromStr for RestartType {
2181    type Err = FoundationError;
2182
2183    fn from_str(s: &str) -> Result<Self, Self::Err> {
2184        match s {
2185            "Continuous" | "continuous" | "0" => Ok(Self::Continuous),
2186            "Section" | "section" | "1" => Ok(Self::Section),
2187            "Page" | "page" | "2" => Ok(Self::Page),
2188            _ => Err(FoundationError::ParseError {
2189                type_name: "RestartType".to_string(),
2190                value: s.to_string(),
2191                valid_values: "Continuous, Section, Page".to_string(),
2192            }),
2193        }
2194    }
2195}
2196
2197impl TryFrom<u8> for RestartType {
2198    type Error = FoundationError;
2199
2200    fn try_from(value: u8) -> Result<Self, Self::Error> {
2201        match value {
2202            0 => Ok(Self::Continuous),
2203            1 => Ok(Self::Section),
2204            2 => Ok(Self::Page),
2205            _ => Err(FoundationError::ParseError {
2206                type_name: "RestartType".to_string(),
2207                value: value.to_string(),
2208                valid_values: "0 (Continuous), 1 (Section), 2 (Page)".to_string(),
2209            }),
2210        }
2211    }
2212}
2213
2214impl schemars::JsonSchema for RestartType {
2215    fn schema_name() -> std::borrow::Cow<'static, str> {
2216        std::borrow::Cow::Borrowed("RestartType")
2217    }
2218
2219    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2220        gen.subschema_for::<String>()
2221    }
2222}
2223
2224// ---------------------------------------------------------------------------
2225// TextBorderType
2226// ---------------------------------------------------------------------------
2227
2228/// Reference frame for page border offset measurement.
2229///
2230/// Controls whether page border offsets are measured from the paper edge
2231/// or from the content area.
2232///
2233/// # Examples
2234///
2235/// ```
2236/// use hwpforge_foundation::TextBorderType;
2237///
2238/// assert_eq!(TextBorderType::default(), TextBorderType::Paper);
2239/// assert_eq!(TextBorderType::Paper.to_string(), "Paper");
2240/// ```
2241#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2242#[non_exhaustive]
2243#[repr(u8)]
2244pub enum TextBorderType {
2245    /// Offsets measured from paper edge (default).
2246    #[default]
2247    Paper = 0,
2248    /// Offsets measured from content area.
2249    Content = 1,
2250}
2251
2252impl fmt::Display for TextBorderType {
2253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2254        match self {
2255            Self::Paper => f.write_str("Paper"),
2256            Self::Content => f.write_str("Content"),
2257        }
2258    }
2259}
2260
2261impl std::str::FromStr for TextBorderType {
2262    type Err = FoundationError;
2263
2264    fn from_str(s: &str) -> Result<Self, Self::Err> {
2265        match s {
2266            "Paper" | "PAPER" | "paper" => Ok(Self::Paper),
2267            "Content" | "CONTENT" | "content" => Ok(Self::Content),
2268            _ => Err(FoundationError::ParseError {
2269                type_name: "TextBorderType".to_string(),
2270                value: s.to_string(),
2271                valid_values: "Paper, Content".to_string(),
2272            }),
2273        }
2274    }
2275}
2276
2277impl TryFrom<u8> for TextBorderType {
2278    type Error = FoundationError;
2279
2280    fn try_from(value: u8) -> Result<Self, Self::Error> {
2281        match value {
2282            0 => Ok(Self::Paper),
2283            1 => Ok(Self::Content),
2284            _ => Err(FoundationError::ParseError {
2285                type_name: "TextBorderType".to_string(),
2286                value: value.to_string(),
2287                valid_values: "0 (Paper), 1 (Content)".to_string(),
2288            }),
2289        }
2290    }
2291}
2292
2293impl schemars::JsonSchema for TextBorderType {
2294    fn schema_name() -> std::borrow::Cow<'static, str> {
2295        std::borrow::Cow::Borrowed("TextBorderType")
2296    }
2297
2298    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2299        gen.subschema_for::<String>()
2300    }
2301}
2302
2303// ---------------------------------------------------------------------------
2304// Flip
2305// ---------------------------------------------------------------------------
2306
2307/// Flip/mirror state for drawing shapes.
2308///
2309/// Controls horizontal and/or vertical mirroring of a shape.
2310///
2311/// # Examples
2312///
2313/// ```
2314/// use hwpforge_foundation::Flip;
2315///
2316/// assert_eq!(Flip::default(), Flip::None);
2317/// assert_eq!(Flip::Horizontal.to_string(), "Horizontal");
2318/// ```
2319#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2320#[non_exhaustive]
2321#[repr(u8)]
2322pub enum Flip {
2323    /// No flip (default).
2324    #[default]
2325    None = 0,
2326    /// Mirrored horizontally.
2327    Horizontal = 1,
2328    /// Mirrored vertically.
2329    Vertical = 2,
2330    /// Mirrored both horizontally and vertically.
2331    Both = 3,
2332}
2333
2334impl fmt::Display for Flip {
2335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2336        match self {
2337            Self::None => f.write_str("None"),
2338            Self::Horizontal => f.write_str("Horizontal"),
2339            Self::Vertical => f.write_str("Vertical"),
2340            Self::Both => f.write_str("Both"),
2341        }
2342    }
2343}
2344
2345impl std::str::FromStr for Flip {
2346    type Err = FoundationError;
2347
2348    fn from_str(s: &str) -> Result<Self, Self::Err> {
2349        match s {
2350            "None" | "NONE" | "none" => Ok(Self::None),
2351            "Horizontal" | "HORIZONTAL" | "horizontal" => Ok(Self::Horizontal),
2352            "Vertical" | "VERTICAL" | "vertical" => Ok(Self::Vertical),
2353            "Both" | "BOTH" | "both" => Ok(Self::Both),
2354            _ => Err(FoundationError::ParseError {
2355                type_name: "Flip".to_string(),
2356                value: s.to_string(),
2357                valid_values: "None, Horizontal, Vertical, Both".to_string(),
2358            }),
2359        }
2360    }
2361}
2362
2363impl TryFrom<u8> for Flip {
2364    type Error = FoundationError;
2365
2366    fn try_from(value: u8) -> Result<Self, Self::Error> {
2367        match value {
2368            0 => Ok(Self::None),
2369            1 => Ok(Self::Horizontal),
2370            2 => Ok(Self::Vertical),
2371            3 => Ok(Self::Both),
2372            _ => Err(FoundationError::ParseError {
2373                type_name: "Flip".to_string(),
2374                value: value.to_string(),
2375                valid_values: "0 (None), 1 (Horizontal), 2 (Vertical), 3 (Both)".to_string(),
2376            }),
2377        }
2378    }
2379}
2380
2381impl schemars::JsonSchema for Flip {
2382    fn schema_name() -> std::borrow::Cow<'static, str> {
2383        std::borrow::Cow::Borrowed("Flip")
2384    }
2385
2386    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2387        gen.subschema_for::<String>()
2388    }
2389}
2390
2391// ---------------------------------------------------------------------------
2392// ArcType
2393// ---------------------------------------------------------------------------
2394
2395/// Arc drawing type for ellipse-based arc shapes.
2396///
2397/// # Examples
2398///
2399/// ```
2400/// use hwpforge_foundation::ArcType;
2401///
2402/// assert_eq!(ArcType::default(), ArcType::Normal);
2403/// ```
2404#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2405#[non_exhaustive]
2406#[repr(u8)]
2407pub enum ArcType {
2408    /// Open arc (just the curved edge).
2409    #[default]
2410    Normal = 0,
2411    /// Pie/sector (arc + two radii closing to center).
2412    Pie = 1,
2413    /// Chord (arc + straight line closing endpoints).
2414    Chord = 2,
2415}
2416
2417impl fmt::Display for ArcType {
2418    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2419        match self {
2420            Self::Normal => f.write_str("NORMAL"),
2421            Self::Pie => f.write_str("PIE"),
2422            Self::Chord => f.write_str("CHORD"),
2423        }
2424    }
2425}
2426
2427impl std::str::FromStr for ArcType {
2428    type Err = FoundationError;
2429
2430    fn from_str(s: &str) -> Result<Self, Self::Err> {
2431        match s {
2432            "NORMAL" | "Normal" | "normal" => Ok(Self::Normal),
2433            "PIE" | "Pie" | "pie" => Ok(Self::Pie),
2434            "CHORD" | "Chord" | "chord" => Ok(Self::Chord),
2435            _ => Err(FoundationError::ParseError {
2436                type_name: "ArcType".to_string(),
2437                value: s.to_string(),
2438                valid_values: "NORMAL, PIE, CHORD".to_string(),
2439            }),
2440        }
2441    }
2442}
2443
2444impl TryFrom<u8> for ArcType {
2445    type Error = FoundationError;
2446
2447    fn try_from(value: u8) -> Result<Self, Self::Error> {
2448        match value {
2449            0 => Ok(Self::Normal),
2450            1 => Ok(Self::Pie),
2451            2 => Ok(Self::Chord),
2452            _ => Err(FoundationError::ParseError {
2453                type_name: "ArcType".to_string(),
2454                value: value.to_string(),
2455                valid_values: "0 (Normal), 1 (Pie), 2 (Chord)".to_string(),
2456            }),
2457        }
2458    }
2459}
2460
2461impl schemars::JsonSchema for ArcType {
2462    fn schema_name() -> std::borrow::Cow<'static, str> {
2463        std::borrow::Cow::Borrowed("ArcType")
2464    }
2465
2466    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2467        gen.subschema_for::<String>()
2468    }
2469}
2470
2471// ---------------------------------------------------------------------------
2472// ArrowType
2473// ---------------------------------------------------------------------------
2474
2475/// Arrowhead shape for line endpoints.
2476///
2477/// # Examples
2478///
2479/// ```
2480/// use hwpforge_foundation::ArrowType;
2481///
2482/// assert_eq!(ArrowType::default(), ArrowType::None);
2483/// ```
2484#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2485#[non_exhaustive]
2486#[repr(u8)]
2487pub enum ArrowType {
2488    /// No arrowhead (default).
2489    #[default]
2490    None = 0,
2491    /// Standard filled arrowhead.
2492    Normal = 1,
2493    /// Arrow-shaped arrowhead.
2494    Arrow = 2,
2495    /// Concave arrowhead.
2496    Concave = 3,
2497    /// Diamond arrowhead.
2498    Diamond = 4,
2499    /// Oval/circle arrowhead.
2500    Oval = 5,
2501    /// Open (unfilled) arrowhead.
2502    Open = 6,
2503}
2504
2505impl fmt::Display for ArrowType {
2506    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2507        // KS X 6101 ArrowType values.
2508        // Diamond/Oval/Open default to FILLED_ variants here;
2509        // the encoder resolves FILLED_ vs EMPTY_ based on ArrowStyle.filled.
2510        match self {
2511            Self::None => f.write_str("NORMAL"),
2512            Self::Normal => f.write_str("ARROW"),
2513            Self::Arrow => f.write_str("SPEAR"),
2514            Self::Concave => f.write_str("CONCAVE_ARROW"),
2515            Self::Diamond => f.write_str("FILLED_DIAMOND"),
2516            Self::Oval => f.write_str("FILLED_CIRCLE"),
2517            Self::Open => f.write_str("EMPTY_BOX"),
2518        }
2519    }
2520}
2521
2522impl std::str::FromStr for ArrowType {
2523    type Err = FoundationError;
2524
2525    fn from_str(s: &str) -> Result<Self, Self::Err> {
2526        // KS X 6101 ArrowType values (primary) + legacy aliases for backward compat.
2527        match s {
2528            "NORMAL" => Ok(Self::None),
2529            "ARROW" => Ok(Self::Normal),
2530            "SPEAR" => Ok(Self::Arrow),
2531            "CONCAVE_ARROW" => Ok(Self::Concave),
2532            "FILLED_DIAMOND" | "EMPTY_DIAMOND" => Ok(Self::Diamond),
2533            "FILLED_CIRCLE" | "EMPTY_CIRCLE" => Ok(Self::Oval),
2534            "FILLED_BOX" | "EMPTY_BOX" => Ok(Self::Open),
2535            _ => Err(FoundationError::ParseError {
2536                type_name: "ArrowType".to_string(),
2537                value: s.to_string(),
2538                valid_values: "NORMAL, ARROW, SPEAR, CONCAVE_ARROW, FILLED_DIAMOND, EMPTY_DIAMOND, FILLED_CIRCLE, EMPTY_CIRCLE, FILLED_BOX, EMPTY_BOX"
2539                    .to_string(),
2540            }),
2541        }
2542    }
2543}
2544
2545impl TryFrom<u8> for ArrowType {
2546    type Error = FoundationError;
2547
2548    fn try_from(value: u8) -> Result<Self, Self::Error> {
2549        match value {
2550            0 => Ok(Self::None),
2551            1 => Ok(Self::Normal),
2552            2 => Ok(Self::Arrow),
2553            3 => Ok(Self::Concave),
2554            4 => Ok(Self::Diamond),
2555            5 => Ok(Self::Oval),
2556            6 => Ok(Self::Open),
2557            _ => Err(FoundationError::ParseError {
2558                type_name: "ArrowType".to_string(),
2559                value: value.to_string(),
2560                valid_values:
2561                    "0 (None), 1 (Normal), 2 (Arrow), 3 (Concave), 4 (Diamond), 5 (Oval), 6 (Open)"
2562                        .to_string(),
2563            }),
2564        }
2565    }
2566}
2567
2568impl schemars::JsonSchema for ArrowType {
2569    fn schema_name() -> std::borrow::Cow<'static, str> {
2570        std::borrow::Cow::Borrowed("ArrowType")
2571    }
2572
2573    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2574        gen.subschema_for::<String>()
2575    }
2576}
2577
2578// ---------------------------------------------------------------------------
2579// ArrowSize
2580// ---------------------------------------------------------------------------
2581
2582/// Arrowhead size for line endpoints.
2583///
2584/// Encoded as `{HEAD}_{TAIL}` string in HWPX (e.g. `"MEDIUM_MEDIUM"`).
2585///
2586/// # Examples
2587///
2588/// ```
2589/// use hwpforge_foundation::ArrowSize;
2590///
2591/// assert_eq!(ArrowSize::default(), ArrowSize::Medium);
2592/// ```
2593#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2594#[non_exhaustive]
2595#[repr(u8)]
2596pub enum ArrowSize {
2597    /// Small arrowhead.
2598    Small = 0,
2599    /// Medium arrowhead (default).
2600    #[default]
2601    Medium = 1,
2602    /// Large arrowhead.
2603    Large = 2,
2604}
2605
2606impl fmt::Display for ArrowSize {
2607    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2608        match self {
2609            Self::Small => f.write_str("SMALL_SMALL"),
2610            Self::Medium => f.write_str("MEDIUM_MEDIUM"),
2611            Self::Large => f.write_str("LARGE_LARGE"),
2612        }
2613    }
2614}
2615
2616impl std::str::FromStr for ArrowSize {
2617    type Err = FoundationError;
2618
2619    fn from_str(s: &str) -> Result<Self, Self::Err> {
2620        match s {
2621            "SMALL_SMALL" | "Small" | "small" => Ok(Self::Small),
2622            "MEDIUM_MEDIUM" | "Medium" | "medium" => Ok(Self::Medium),
2623            "LARGE_LARGE" | "Large" | "large" => Ok(Self::Large),
2624            _ => Err(FoundationError::ParseError {
2625                type_name: "ArrowSize".to_string(),
2626                value: s.to_string(),
2627                valid_values: "SMALL_SMALL, MEDIUM_MEDIUM, LARGE_LARGE".to_string(),
2628            }),
2629        }
2630    }
2631}
2632
2633impl TryFrom<u8> for ArrowSize {
2634    type Error = FoundationError;
2635
2636    fn try_from(value: u8) -> Result<Self, Self::Error> {
2637        match value {
2638            0 => Ok(Self::Small),
2639            1 => Ok(Self::Medium),
2640            2 => Ok(Self::Large),
2641            _ => Err(FoundationError::ParseError {
2642                type_name: "ArrowSize".to_string(),
2643                value: value.to_string(),
2644                valid_values: "0 (Small), 1 (Medium), 2 (Large)".to_string(),
2645            }),
2646        }
2647    }
2648}
2649
2650impl schemars::JsonSchema for ArrowSize {
2651    fn schema_name() -> std::borrow::Cow<'static, str> {
2652        std::borrow::Cow::Borrowed("ArrowSize")
2653    }
2654
2655    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2656        gen.subschema_for::<String>()
2657    }
2658}
2659
2660// ---------------------------------------------------------------------------
2661// GradientType
2662// ---------------------------------------------------------------------------
2663
2664/// Gradient fill direction type.
2665///
2666/// # Examples
2667///
2668/// ```
2669/// use hwpforge_foundation::GradientType;
2670///
2671/// assert_eq!(GradientType::default(), GradientType::Linear);
2672/// ```
2673#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2674#[non_exhaustive]
2675#[repr(u8)]
2676pub enum GradientType {
2677    /// Linear gradient (default).
2678    #[default]
2679    Linear = 0,
2680    /// Radial gradient (from center outward).
2681    Radial = 1,
2682    /// Square/rectangular gradient.
2683    Square = 2,
2684    /// Conical gradient (angular sweep).
2685    Conical = 3,
2686}
2687
2688impl fmt::Display for GradientType {
2689    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2690        match self {
2691            Self::Linear => f.write_str("LINEAR"),
2692            Self::Radial => f.write_str("RADIAL"),
2693            Self::Square => f.write_str("SQUARE"),
2694            Self::Conical => f.write_str("CONICAL"),
2695        }
2696    }
2697}
2698
2699impl std::str::FromStr for GradientType {
2700    type Err = FoundationError;
2701
2702    fn from_str(s: &str) -> Result<Self, Self::Err> {
2703        match s {
2704            "LINEAR" | "Linear" | "linear" => Ok(Self::Linear),
2705            "RADIAL" | "Radial" | "radial" => Ok(Self::Radial),
2706            "SQUARE" | "Square" | "square" => Ok(Self::Square),
2707            "CONICAL" | "Conical" | "conical" => Ok(Self::Conical),
2708            _ => Err(FoundationError::ParseError {
2709                type_name: "GradientType".to_string(),
2710                value: s.to_string(),
2711                valid_values: "LINEAR, RADIAL, SQUARE, CONICAL".to_string(),
2712            }),
2713        }
2714    }
2715}
2716
2717impl TryFrom<u8> for GradientType {
2718    type Error = FoundationError;
2719
2720    fn try_from(value: u8) -> Result<Self, Self::Error> {
2721        match value {
2722            0 => Ok(Self::Linear),
2723            1 => Ok(Self::Radial),
2724            2 => Ok(Self::Square),
2725            3 => Ok(Self::Conical),
2726            _ => Err(FoundationError::ParseError {
2727                type_name: "GradientType".to_string(),
2728                value: value.to_string(),
2729                valid_values: "0 (Linear), 1 (Radial), 2 (Square), 3 (Conical)".to_string(),
2730            }),
2731        }
2732    }
2733}
2734
2735impl schemars::JsonSchema for GradientType {
2736    fn schema_name() -> std::borrow::Cow<'static, str> {
2737        std::borrow::Cow::Borrowed("GradientType")
2738    }
2739
2740    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2741        gen.subschema_for::<String>()
2742    }
2743}
2744
2745// ---------------------------------------------------------------------------
2746// PatternType
2747// ---------------------------------------------------------------------------
2748
2749/// Hatch/pattern fill type for shapes.
2750///
2751/// # Examples
2752///
2753/// ```
2754/// use hwpforge_foundation::PatternType;
2755///
2756/// assert_eq!(PatternType::default(), PatternType::Horizontal);
2757/// ```
2758#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2759#[non_exhaustive]
2760#[repr(u8)]
2761pub enum PatternType {
2762    /// Horizontal lines (default).
2763    #[default]
2764    Horizontal = 0,
2765    /// Vertical lines.
2766    Vertical = 1,
2767    /// Backslash diagonal lines.
2768    BackSlash = 2,
2769    /// Forward slash diagonal lines.
2770    Slash = 3,
2771    /// Cross-hatch (horizontal + vertical).
2772    Cross = 4,
2773    /// Cross-diagonal hatch.
2774    CrossDiagonal = 5,
2775}
2776
2777impl fmt::Display for PatternType {
2778    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2779        // 한글 renders BACK_SLASH as `/` and SLASH as `\` — opposite to KS X 6101 XSD docs.
2780        // We swap the mapping so our semantic enum values match actual rendering.
2781        match self {
2782            Self::Horizontal => f.write_str("HORIZONTAL"),
2783            Self::Vertical => f.write_str("VERTICAL"),
2784            Self::BackSlash => f.write_str("SLASH"),
2785            Self::Slash => f.write_str("BACK_SLASH"),
2786            Self::Cross => f.write_str("CROSS"),
2787            Self::CrossDiagonal => f.write_str("CROSS_DIAGONAL"),
2788        }
2789    }
2790}
2791
2792impl std::str::FromStr for PatternType {
2793    type Err = FoundationError;
2794
2795    fn from_str(s: &str) -> Result<Self, Self::Err> {
2796        // Swapped BACK_SLASH/SLASH to match Display (한글 renders them opposite to spec).
2797        // Only SCREAMING_CASE forms used here — PascalCase comes through serde derive.
2798        match s {
2799            "HORIZONTAL" | "horizontal" => Ok(Self::Horizontal),
2800            "VERTICAL" | "vertical" => Ok(Self::Vertical),
2801            "BACK_SLASH" | "backslash" => Ok(Self::Slash),
2802            "SLASH" | "slash" => Ok(Self::BackSlash),
2803            "CROSS" | "cross" => Ok(Self::Cross),
2804            "CROSS_DIAGONAL" | "crossdiagonal" => Ok(Self::CrossDiagonal),
2805            _ => Err(FoundationError::ParseError {
2806                type_name: "PatternType".to_string(),
2807                value: s.to_string(),
2808                valid_values: "HORIZONTAL, VERTICAL, BACK_SLASH, SLASH, CROSS, CROSS_DIAGONAL"
2809                    .to_string(),
2810            }),
2811        }
2812    }
2813}
2814
2815impl TryFrom<u8> for PatternType {
2816    type Error = FoundationError;
2817
2818    fn try_from(value: u8) -> Result<Self, Self::Error> {
2819        match value {
2820            0 => Ok(Self::Horizontal),
2821            1 => Ok(Self::Vertical),
2822            2 => Ok(Self::BackSlash),
2823            3 => Ok(Self::Slash),
2824            4 => Ok(Self::Cross),
2825            5 => Ok(Self::CrossDiagonal),
2826            _ => Err(FoundationError::ParseError {
2827                type_name: "PatternType".to_string(),
2828                value: value.to_string(),
2829                valid_values:
2830                    "0 (Horizontal), 1 (Vertical), 2 (BackSlash), 3 (Slash), 4 (Cross), 5 (CrossDiagonal)"
2831                        .to_string(),
2832            }),
2833        }
2834    }
2835}
2836
2837impl schemars::JsonSchema for PatternType {
2838    fn schema_name() -> std::borrow::Cow<'static, str> {
2839        std::borrow::Cow::Borrowed("PatternType")
2840    }
2841
2842    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2843        gen.subschema_for::<String>()
2844    }
2845}
2846
2847// ---------------------------------------------------------------------------
2848// ImageFillMode
2849// ---------------------------------------------------------------------------
2850
2851/// How an image is fitted within a shape fill area.
2852///
2853/// # Examples
2854///
2855/// ```
2856/// use hwpforge_foundation::ImageFillMode;
2857///
2858/// assert_eq!(ImageFillMode::default(), ImageFillMode::Tile);
2859/// ```
2860#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2861#[non_exhaustive]
2862#[repr(u8)]
2863pub enum ImageFillMode {
2864    /// Tile the image to fill the area (default).
2865    #[default]
2866    Tile = 0,
2867    /// Center the image without scaling.
2868    Center = 1,
2869    /// Stretch the image to fit exactly.
2870    Stretch = 2,
2871    /// Scale proportionally to fit all within the area.
2872    FitAll = 3,
2873}
2874
2875impl fmt::Display for ImageFillMode {
2876    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2877        match self {
2878            Self::Tile => f.write_str("TILE"),
2879            Self::Center => f.write_str("CENTER"),
2880            Self::Stretch => f.write_str("STRETCH"),
2881            Self::FitAll => f.write_str("FIT_ALL"),
2882        }
2883    }
2884}
2885
2886impl std::str::FromStr for ImageFillMode {
2887    type Err = FoundationError;
2888
2889    fn from_str(s: &str) -> Result<Self, Self::Err> {
2890        match s {
2891            "TILE" | "Tile" | "tile" => Ok(Self::Tile),
2892            "CENTER" | "Center" | "center" => Ok(Self::Center),
2893            "STRETCH" | "Stretch" | "stretch" => Ok(Self::Stretch),
2894            "FIT_ALL" | "FitAll" | "fit_all" => Ok(Self::FitAll),
2895            _ => Err(FoundationError::ParseError {
2896                type_name: "ImageFillMode".to_string(),
2897                value: s.to_string(),
2898                valid_values: "TILE, CENTER, STRETCH, FIT_ALL".to_string(),
2899            }),
2900        }
2901    }
2902}
2903
2904impl TryFrom<u8> for ImageFillMode {
2905    type Error = FoundationError;
2906
2907    fn try_from(value: u8) -> Result<Self, Self::Error> {
2908        match value {
2909            0 => Ok(Self::Tile),
2910            1 => Ok(Self::Center),
2911            2 => Ok(Self::Stretch),
2912            3 => Ok(Self::FitAll),
2913            _ => Err(FoundationError::ParseError {
2914                type_name: "ImageFillMode".to_string(),
2915                value: value.to_string(),
2916                valid_values: "0 (Tile), 1 (Center), 2 (Stretch), 3 (FitAll)".to_string(),
2917            }),
2918        }
2919    }
2920}
2921
2922impl schemars::JsonSchema for ImageFillMode {
2923    fn schema_name() -> std::borrow::Cow<'static, str> {
2924        std::borrow::Cow::Borrowed("ImageFillMode")
2925    }
2926
2927    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
2928        gen.subschema_for::<String>()
2929    }
2930}
2931
2932// ---------------------------------------------------------------------------
2933// CurveSegmentType
2934// ---------------------------------------------------------------------------
2935
2936/// Segment type within a curve path.
2937///
2938/// # Examples
2939///
2940/// ```
2941/// use hwpforge_foundation::CurveSegmentType;
2942///
2943/// assert_eq!(CurveSegmentType::default(), CurveSegmentType::Line);
2944/// ```
2945#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
2946#[non_exhaustive]
2947#[repr(u8)]
2948pub enum CurveSegmentType {
2949    /// Straight line segment (default).
2950    #[default]
2951    Line = 0,
2952    /// Cubic bezier curve segment.
2953    Curve = 1,
2954}
2955
2956impl fmt::Display for CurveSegmentType {
2957    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2958        match self {
2959            Self::Line => f.write_str("LINE"),
2960            Self::Curve => f.write_str("CURVE"),
2961        }
2962    }
2963}
2964
2965impl std::str::FromStr for CurveSegmentType {
2966    type Err = FoundationError;
2967
2968    fn from_str(s: &str) -> Result<Self, Self::Err> {
2969        match s {
2970            "LINE" | "Line" | "line" => Ok(Self::Line),
2971            "CURVE" | "Curve" | "curve" => Ok(Self::Curve),
2972            _ => Err(FoundationError::ParseError {
2973                type_name: "CurveSegmentType".to_string(),
2974                value: s.to_string(),
2975                valid_values: "LINE, CURVE".to_string(),
2976            }),
2977        }
2978    }
2979}
2980
2981impl TryFrom<u8> for CurveSegmentType {
2982    type Error = FoundationError;
2983
2984    fn try_from(value: u8) -> Result<Self, Self::Error> {
2985        match value {
2986            0 => Ok(Self::Line),
2987            1 => Ok(Self::Curve),
2988            _ => Err(FoundationError::ParseError {
2989                type_name: "CurveSegmentType".to_string(),
2990                value: value.to_string(),
2991                valid_values: "0 (Line), 1 (Curve)".to_string(),
2992            }),
2993        }
2994    }
2995}
2996
2997impl schemars::JsonSchema for CurveSegmentType {
2998    fn schema_name() -> std::borrow::Cow<'static, str> {
2999        std::borrow::Cow::Borrowed("CurveSegmentType")
3000    }
3001
3002    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3003        gen.subschema_for::<String>()
3004    }
3005}
3006
3007// ---------------------------------------------------------------------------
3008// BookmarkType
3009// ---------------------------------------------------------------------------
3010
3011/// Type of bookmark in an HWPX document.
3012///
3013/// Bookmarks can mark a single point or span a range of content
3014/// (start/end pair using `fieldBegin`/`fieldEnd`).
3015#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
3016#[non_exhaustive]
3017#[repr(u8)]
3018pub enum BookmarkType {
3019    /// A point bookmark at a single location (direct serde in `<hp:ctrl>`).
3020    #[default]
3021    Point = 0,
3022    /// Start of a span bookmark (`fieldBegin type="BOOKMARK"`).
3023    SpanStart = 1,
3024    /// End of a span bookmark (`fieldEnd beginIDRef`).
3025    SpanEnd = 2,
3026}
3027
3028impl fmt::Display for BookmarkType {
3029    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3030        match self {
3031            Self::Point => f.write_str("Point"),
3032            Self::SpanStart => f.write_str("SpanStart"),
3033            Self::SpanEnd => f.write_str("SpanEnd"),
3034        }
3035    }
3036}
3037
3038impl std::str::FromStr for BookmarkType {
3039    type Err = FoundationError;
3040
3041    fn from_str(s: &str) -> Result<Self, Self::Err> {
3042        match s {
3043            "Point" | "point" => Ok(Self::Point),
3044            "SpanStart" | "span_start" => Ok(Self::SpanStart),
3045            "SpanEnd" | "span_end" => Ok(Self::SpanEnd),
3046            _ => Err(FoundationError::ParseError {
3047                type_name: "BookmarkType".to_string(),
3048                value: s.to_string(),
3049                valid_values: "Point, SpanStart, SpanEnd".to_string(),
3050            }),
3051        }
3052    }
3053}
3054
3055impl TryFrom<u8> for BookmarkType {
3056    type Error = FoundationError;
3057
3058    fn try_from(value: u8) -> Result<Self, Self::Error> {
3059        match value {
3060            0 => Ok(Self::Point),
3061            1 => Ok(Self::SpanStart),
3062            2 => Ok(Self::SpanEnd),
3063            _ => Err(FoundationError::ParseError {
3064                type_name: "BookmarkType".to_string(),
3065                value: value.to_string(),
3066                valid_values: "0 (Point), 1 (SpanStart), 2 (SpanEnd)".to_string(),
3067            }),
3068        }
3069    }
3070}
3071
3072impl schemars::JsonSchema for BookmarkType {
3073    fn schema_name() -> std::borrow::Cow<'static, str> {
3074        std::borrow::Cow::Borrowed("BookmarkType")
3075    }
3076
3077    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3078        gen.subschema_for::<String>()
3079    }
3080}
3081
3082// ---------------------------------------------------------------------------
3083// FieldType
3084// ---------------------------------------------------------------------------
3085
3086/// Type of a press-field (누름틀) in an HWPX document.
3087///
3088/// Press-fields are interactive form fields that users can click to fill in.
3089#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
3090#[non_exhaustive]
3091#[repr(u8)]
3092pub enum FieldType {
3093    /// Click-here placeholder field (default).
3094    #[default]
3095    ClickHere = 0,
3096    /// Automatic date field.
3097    Date = 1,
3098    /// Automatic time field.
3099    Time = 2,
3100    /// Page number field.
3101    PageNum = 3,
3102    /// Document summary field.
3103    DocSummary = 4,
3104    /// User information field.
3105    UserInfo = 5,
3106}
3107
3108impl fmt::Display for FieldType {
3109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3110        match self {
3111            Self::ClickHere => f.write_str("CLICK_HERE"),
3112            Self::Date => f.write_str("DATE"),
3113            Self::Time => f.write_str("TIME"),
3114            Self::PageNum => f.write_str("PAGE_NUM"),
3115            Self::DocSummary => f.write_str("DOC_SUMMARY"),
3116            Self::UserInfo => f.write_str("USER_INFO"),
3117        }
3118    }
3119}
3120
3121impl std::str::FromStr for FieldType {
3122    type Err = FoundationError;
3123
3124    fn from_str(s: &str) -> Result<Self, Self::Err> {
3125        match s {
3126            "CLICK_HERE" | "ClickHere" | "click_here" => Ok(Self::ClickHere),
3127            "DATE" | "Date" | "date" => Ok(Self::Date),
3128            "TIME" | "Time" | "time" => Ok(Self::Time),
3129            "PAGE_NUM" | "PageNum" | "page_num" => Ok(Self::PageNum),
3130            "DOC_SUMMARY" | "DocSummary" | "doc_summary" => Ok(Self::DocSummary),
3131            "USER_INFO" | "UserInfo" | "user_info" => Ok(Self::UserInfo),
3132            _ => Err(FoundationError::ParseError {
3133                type_name: "FieldType".to_string(),
3134                value: s.to_string(),
3135                valid_values: "CLICK_HERE, DATE, TIME, PAGE_NUM, DOC_SUMMARY, USER_INFO"
3136                    .to_string(),
3137            }),
3138        }
3139    }
3140}
3141
3142impl TryFrom<u8> for FieldType {
3143    type Error = FoundationError;
3144
3145    fn try_from(value: u8) -> Result<Self, Self::Error> {
3146        match value {
3147            0 => Ok(Self::ClickHere),
3148            1 => Ok(Self::Date),
3149            2 => Ok(Self::Time),
3150            3 => Ok(Self::PageNum),
3151            4 => Ok(Self::DocSummary),
3152            5 => Ok(Self::UserInfo),
3153            _ => Err(FoundationError::ParseError {
3154                type_name: "FieldType".to_string(),
3155                value: value.to_string(),
3156                valid_values: "0..5 (ClickHere..UserInfo)".to_string(),
3157            }),
3158        }
3159    }
3160}
3161
3162impl schemars::JsonSchema for FieldType {
3163    fn schema_name() -> std::borrow::Cow<'static, str> {
3164        std::borrow::Cow::Borrowed("FieldType")
3165    }
3166
3167    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3168        gen.subschema_for::<String>()
3169    }
3170}
3171
3172// ---------------------------------------------------------------------------
3173// RefType
3174// ---------------------------------------------------------------------------
3175
3176/// Target type of a cross-reference (상호참조).
3177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
3178#[non_exhaustive]
3179#[repr(u8)]
3180pub enum RefType {
3181    /// Reference to a bookmark target.
3182    #[default]
3183    Bookmark = 0,
3184    /// Reference to a table caption number.
3185    Table = 1,
3186    /// Reference to a figure/image caption number.
3187    Figure = 2,
3188    /// Reference to an equation number.
3189    Equation = 3,
3190}
3191
3192impl fmt::Display for RefType {
3193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3194        match self {
3195            Self::Bookmark => f.write_str("TARGET_BOOKMARK"),
3196            Self::Table => f.write_str("TARGET_TABLE"),
3197            Self::Figure => f.write_str("TARGET_FIGURE"),
3198            Self::Equation => f.write_str("TARGET_EQUATION"),
3199        }
3200    }
3201}
3202
3203impl std::str::FromStr for RefType {
3204    type Err = FoundationError;
3205
3206    fn from_str(s: &str) -> Result<Self, Self::Err> {
3207        match s {
3208            "TARGET_BOOKMARK" | "Bookmark" | "bookmark" => Ok(Self::Bookmark),
3209            "TARGET_TABLE" | "Table" | "table" => Ok(Self::Table),
3210            "TARGET_FIGURE" | "Figure" | "figure" => Ok(Self::Figure),
3211            "TARGET_EQUATION" | "Equation" | "equation" => Ok(Self::Equation),
3212            _ => Err(FoundationError::ParseError {
3213                type_name: "RefType".to_string(),
3214                value: s.to_string(),
3215                valid_values: "TARGET_BOOKMARK, TARGET_TABLE, TARGET_FIGURE, TARGET_EQUATION"
3216                    .to_string(),
3217            }),
3218        }
3219    }
3220}
3221
3222impl TryFrom<u8> for RefType {
3223    type Error = FoundationError;
3224
3225    fn try_from(value: u8) -> Result<Self, Self::Error> {
3226        match value {
3227            0 => Ok(Self::Bookmark),
3228            1 => Ok(Self::Table),
3229            2 => Ok(Self::Figure),
3230            3 => Ok(Self::Equation),
3231            _ => Err(FoundationError::ParseError {
3232                type_name: "RefType".to_string(),
3233                value: value.to_string(),
3234                valid_values: "0..3 (Bookmark..Equation)".to_string(),
3235            }),
3236        }
3237    }
3238}
3239
3240impl schemars::JsonSchema for RefType {
3241    fn schema_name() -> std::borrow::Cow<'static, str> {
3242        std::borrow::Cow::Borrowed("RefType")
3243    }
3244
3245    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3246        gen.subschema_for::<String>()
3247    }
3248}
3249
3250// ---------------------------------------------------------------------------
3251// RefContentType
3252// ---------------------------------------------------------------------------
3253
3254/// Content display type for a cross-reference (what to show at the reference site).
3255#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
3256#[non_exhaustive]
3257#[repr(u8)]
3258pub enum RefContentType {
3259    /// Show page number where the target appears.
3260    #[default]
3261    Page = 0,
3262    /// Show the target's numbering (e.g. "표 3", "그림 2").
3263    Number = 1,
3264    /// Show the target's content text.
3265    Contents = 2,
3266    /// Show relative position ("위" / "아래").
3267    UpDownPos = 3,
3268}
3269
3270impl fmt::Display for RefContentType {
3271    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3272        match self {
3273            Self::Page => f.write_str("OBJECT_TYPE_PAGE"),
3274            Self::Number => f.write_str("OBJECT_TYPE_NUMBER"),
3275            Self::Contents => f.write_str("OBJECT_TYPE_CONTENTS"),
3276            Self::UpDownPos => f.write_str("OBJECT_TYPE_UPDOWNPOS"),
3277        }
3278    }
3279}
3280
3281impl std::str::FromStr for RefContentType {
3282    type Err = FoundationError;
3283
3284    fn from_str(s: &str) -> Result<Self, Self::Err> {
3285        match s {
3286            "OBJECT_TYPE_PAGE" | "Page" | "page" => Ok(Self::Page),
3287            "OBJECT_TYPE_NUMBER" | "Number" | "number" => Ok(Self::Number),
3288            "OBJECT_TYPE_CONTENTS" | "Contents" | "contents" => Ok(Self::Contents),
3289            "OBJECT_TYPE_UPDOWNPOS" | "UpDownPos" | "updownpos" => Ok(Self::UpDownPos),
3290            _ => Err(FoundationError::ParseError {
3291                type_name: "RefContentType".to_string(),
3292                value: s.to_string(),
3293                valid_values:
3294                    "OBJECT_TYPE_PAGE, OBJECT_TYPE_NUMBER, OBJECT_TYPE_CONTENTS, OBJECT_TYPE_UPDOWNPOS"
3295                        .to_string(),
3296            }),
3297        }
3298    }
3299}
3300
3301impl TryFrom<u8> for RefContentType {
3302    type Error = FoundationError;
3303
3304    fn try_from(value: u8) -> Result<Self, Self::Error> {
3305        match value {
3306            0 => Ok(Self::Page),
3307            1 => Ok(Self::Number),
3308            2 => Ok(Self::Contents),
3309            3 => Ok(Self::UpDownPos),
3310            _ => Err(FoundationError::ParseError {
3311                type_name: "RefContentType".to_string(),
3312                value: value.to_string(),
3313                valid_values: "0..3 (Page..UpDownPos)".to_string(),
3314            }),
3315        }
3316    }
3317}
3318
3319impl schemars::JsonSchema for RefContentType {
3320    fn schema_name() -> std::borrow::Cow<'static, str> {
3321        std::borrow::Cow::Borrowed("RefContentType")
3322    }
3323
3324    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3325        gen.subschema_for::<String>()
3326    }
3327}
3328
3329/// Drop cap style for floating shape objects (HWPX `dropcapstyle` attribute).
3330///
3331/// Controls whether a shape (text box, image, table, etc.) is formatted as a
3332/// drop capital that occupies multiple lines at the start of a paragraph.
3333///
3334/// # HWPX Values
3335///
3336/// | Variant      | HWPX string     |
3337/// |--------------|-----------------|
3338/// | `None`       | `"None"`        |
3339/// | `DoubleLine` | `"DoubleLine"`  |
3340/// | `TripleLine` | `"TripleLine"`  |
3341/// | `Margin`     | `"Margin"`      |
3342#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
3343#[non_exhaustive]
3344pub enum DropCapStyle {
3345    /// No drop cap (default).
3346    #[default]
3347    None = 0,
3348    /// Drop cap spanning 2 lines.
3349    DoubleLine = 1,
3350    /// Drop cap spanning 3 lines.
3351    TripleLine = 2,
3352    /// Drop cap positioned in the margin.
3353    Margin = 3,
3354}
3355
3356impl fmt::Display for DropCapStyle {
3357    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3358        match self {
3359            Self::None => f.write_str("None"),
3360            Self::DoubleLine => f.write_str("DoubleLine"),
3361            Self::TripleLine => f.write_str("TripleLine"),
3362            Self::Margin => f.write_str("Margin"),
3363        }
3364    }
3365}
3366
3367impl DropCapStyle {
3368    /// Parses an HWPX `dropcapstyle` attribute value (PascalCase).
3369    ///
3370    /// Unknown values fall back to `None` (default) for forward compatibility.
3371    pub fn from_hwpx_str(s: &str) -> Self {
3372        match s {
3373            "DoubleLine" => Self::DoubleLine,
3374            "TripleLine" => Self::TripleLine,
3375            "Margin" => Self::Margin,
3376            _ => Self::None,
3377        }
3378    }
3379}
3380
3381impl std::str::FromStr for DropCapStyle {
3382    type Err = FoundationError;
3383
3384    fn from_str(s: &str) -> Result<Self, Self::Err> {
3385        match s {
3386            "None" | "NONE" | "none" => Ok(Self::None),
3387            "DoubleLine" | "DOUBLE_LINE" => Ok(Self::DoubleLine),
3388            "TripleLine" | "TRIPLE_LINE" => Ok(Self::TripleLine),
3389            "Margin" | "MARGIN" => Ok(Self::Margin),
3390            _ => Err(FoundationError::ParseError {
3391                type_name: "DropCapStyle".to_string(),
3392                value: s.to_string(),
3393                valid_values: "None, DoubleLine, TripleLine, Margin".to_string(),
3394            }),
3395        }
3396    }
3397}
3398
3399impl schemars::JsonSchema for DropCapStyle {
3400    fn schema_name() -> std::borrow::Cow<'static, str> {
3401        std::borrow::Cow::Borrowed("DropCapStyle")
3402    }
3403
3404    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3405        gen.subschema_for::<String>()
3406    }
3407}
3408
3409impl serde::Serialize for DropCapStyle {
3410    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3411        serializer.serialize_str(&self.to_string())
3412    }
3413}
3414
3415impl<'de> serde::Deserialize<'de> for DropCapStyle {
3416    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3417        let s = String::deserialize(deserializer)?;
3418        s.parse().map_err(serde::de::Error::custom)
3419    }
3420}
3421
3422// Compile-time size assertions: all enums are 1 byte
3423const _: () = assert!(std::mem::size_of::<DropCapStyle>() == 1);
3424const _: () = assert!(std::mem::size_of::<Alignment>() == 1);
3425const _: () = assert!(std::mem::size_of::<LineSpacingType>() == 1);
3426const _: () = assert!(std::mem::size_of::<BreakType>() == 1);
3427const _: () = assert!(std::mem::size_of::<Language>() == 1);
3428const _: () = assert!(std::mem::size_of::<UnderlineType>() == 1);
3429const _: () = assert!(std::mem::size_of::<StrikeoutShape>() == 1);
3430const _: () = assert!(std::mem::size_of::<OutlineType>() == 1);
3431const _: () = assert!(std::mem::size_of::<ShadowType>() == 1);
3432const _: () = assert!(std::mem::size_of::<EmbossType>() == 1);
3433const _: () = assert!(std::mem::size_of::<EngraveType>() == 1);
3434const _: () = assert!(std::mem::size_of::<VerticalPosition>() == 1);
3435const _: () = assert!(std::mem::size_of::<BorderLineType>() == 1);
3436const _: () = assert!(std::mem::size_of::<FillBrushType>() == 1);
3437const _: () = assert!(std::mem::size_of::<ApplyPageType>() == 1);
3438const _: () = assert!(std::mem::size_of::<NumberFormatType>() == 1);
3439const _: () = assert!(std::mem::size_of::<PageNumberPosition>() == 1);
3440const _: () = assert!(std::mem::size_of::<WordBreakType>() == 1);
3441const _: () = assert!(std::mem::size_of::<EmphasisType>() == 1);
3442const _: () = assert!(std::mem::size_of::<HeadingType>() == 1);
3443const _: () = assert!(std::mem::size_of::<GutterType>() == 1);
3444const _: () = assert!(std::mem::size_of::<ShowMode>() == 1);
3445const _: () = assert!(std::mem::size_of::<RestartType>() == 1);
3446const _: () = assert!(std::mem::size_of::<TextBorderType>() == 1);
3447const _: () = assert!(std::mem::size_of::<Flip>() == 1);
3448const _: () = assert!(std::mem::size_of::<ArcType>() == 1);
3449const _: () = assert!(std::mem::size_of::<ArrowType>() == 1);
3450const _: () = assert!(std::mem::size_of::<ArrowSize>() == 1);
3451const _: () = assert!(std::mem::size_of::<GradientType>() == 1);
3452const _: () = assert!(std::mem::size_of::<PatternType>() == 1);
3453const _: () = assert!(std::mem::size_of::<ImageFillMode>() == 1);
3454const _: () = assert!(std::mem::size_of::<CurveSegmentType>() == 1);
3455const _: () = assert!(std::mem::size_of::<BookmarkType>() == 1);
3456const _: () = assert!(std::mem::size_of::<FieldType>() == 1);
3457const _: () = assert!(std::mem::size_of::<RefType>() == 1);
3458const _: () = assert!(std::mem::size_of::<RefContentType>() == 1);
3459
3460// ---------------------------------------------------------------------------
3461// TextDirection
3462// ---------------------------------------------------------------------------
3463
3464/// Text writing direction for sections and sub-lists.
3465///
3466/// Controls whether text flows horizontally (가로쓰기) or vertically (세로쓰기).
3467/// Used in `<hp:secPr textDirection="...">` and `<hp:subList textDirection="...">`.
3468///
3469/// # Examples
3470///
3471/// ```
3472/// use hwpforge_foundation::TextDirection;
3473///
3474/// assert_eq!(TextDirection::default(), TextDirection::Horizontal);
3475/// assert_eq!(TextDirection::Horizontal.to_string(), "HORIZONTAL");
3476/// ```
3477#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
3478#[non_exhaustive]
3479pub enum TextDirection {
3480    /// Horizontal writing (가로쓰기) — default.
3481    #[default]
3482    Horizontal,
3483    /// Vertical writing with Latin chars rotated 90° (세로쓰기 영문 눕힘).
3484    Vertical,
3485    /// Vertical writing with Latin chars upright (세로쓰기 영문 세움).
3486    VerticalAll,
3487}
3488
3489impl TextDirection {
3490    /// Parses a HWPX XML attribute string (e.g. `"VERTICAL"`).
3491    ///
3492    /// Unknown values fall back to [`TextDirection::Horizontal`].
3493    pub fn from_hwpx_str(s: &str) -> Self {
3494        match s {
3495            "VERTICAL" => Self::Vertical,
3496            "VERTICALALL" => Self::VerticalAll,
3497            _ => Self::Horizontal,
3498        }
3499    }
3500}
3501
3502impl fmt::Display for TextDirection {
3503    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3504        match self {
3505            Self::Horizontal => f.write_str("HORIZONTAL"),
3506            Self::Vertical => f.write_str("VERTICAL"),
3507            Self::VerticalAll => f.write_str("VERTICALALL"),
3508        }
3509    }
3510}
3511
3512impl schemars::JsonSchema for TextDirection {
3513    fn schema_name() -> std::borrow::Cow<'static, str> {
3514        std::borrow::Cow::Borrowed("TextDirection")
3515    }
3516
3517    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3518        gen.subschema_for::<String>()
3519    }
3520}
3521
3522const _: () = assert!(std::mem::size_of::<TextDirection>() == 1);
3523
3524#[cfg(test)]
3525mod tests {
3526    use super::*;
3527    use std::str::FromStr;
3528
3529    // ===================================================================
3530    // Alignment (10+ tests)
3531    // ===================================================================
3532
3533    #[test]
3534    fn alignment_default_is_left() {
3535        assert_eq!(Alignment::default(), Alignment::Left);
3536    }
3537
3538    #[test]
3539    fn alignment_display_all_variants() {
3540        assert_eq!(Alignment::Left.to_string(), "Left");
3541        assert_eq!(Alignment::Center.to_string(), "Center");
3542        assert_eq!(Alignment::Right.to_string(), "Right");
3543        assert_eq!(Alignment::Justify.to_string(), "Justify");
3544        assert_eq!(Alignment::Distribute.to_string(), "Distribute");
3545        assert_eq!(Alignment::DistributeFlush.to_string(), "DistributeFlush");
3546    }
3547
3548    #[test]
3549    fn alignment_from_str_pascal_case() {
3550        assert_eq!(Alignment::from_str("Left").unwrap(), Alignment::Left);
3551        assert_eq!(Alignment::from_str("Center").unwrap(), Alignment::Center);
3552        assert_eq!(Alignment::from_str("Right").unwrap(), Alignment::Right);
3553        assert_eq!(Alignment::from_str("Justify").unwrap(), Alignment::Justify);
3554        assert_eq!(Alignment::from_str("Distribute").unwrap(), Alignment::Distribute);
3555        assert_eq!(Alignment::from_str("DistributeFlush").unwrap(), Alignment::DistributeFlush);
3556    }
3557
3558    #[test]
3559    fn alignment_from_str_lower_case() {
3560        assert_eq!(Alignment::from_str("left").unwrap(), Alignment::Left);
3561        assert_eq!(Alignment::from_str("center").unwrap(), Alignment::Center);
3562        assert_eq!(Alignment::from_str("distribute").unwrap(), Alignment::Distribute);
3563        assert_eq!(Alignment::from_str("distributeflush").unwrap(), Alignment::DistributeFlush);
3564        assert_eq!(Alignment::from_str("distribute_flush").unwrap(), Alignment::DistributeFlush);
3565    }
3566
3567    #[test]
3568    fn alignment_from_str_invalid() {
3569        let err = Alignment::from_str("leftt").unwrap_err();
3570        match err {
3571            FoundationError::ParseError { ref type_name, ref value, .. } => {
3572                assert_eq!(type_name, "Alignment");
3573                assert_eq!(value, "leftt");
3574            }
3575            other => panic!("unexpected: {other}"),
3576        }
3577    }
3578
3579    #[test]
3580    fn alignment_try_from_u8() {
3581        assert_eq!(Alignment::try_from(0u8).unwrap(), Alignment::Left);
3582        assert_eq!(Alignment::try_from(1u8).unwrap(), Alignment::Center);
3583        assert_eq!(Alignment::try_from(2u8).unwrap(), Alignment::Right);
3584        assert_eq!(Alignment::try_from(3u8).unwrap(), Alignment::Justify);
3585        assert_eq!(Alignment::try_from(4u8).unwrap(), Alignment::Distribute);
3586        assert_eq!(Alignment::try_from(5u8).unwrap(), Alignment::DistributeFlush);
3587        assert!(Alignment::try_from(6u8).is_err());
3588        assert!(Alignment::try_from(255u8).is_err());
3589    }
3590
3591    #[test]
3592    fn alignment_repr_values() {
3593        assert_eq!(Alignment::Left as u8, 0);
3594        assert_eq!(Alignment::Center as u8, 1);
3595        assert_eq!(Alignment::Right as u8, 2);
3596        assert_eq!(Alignment::Justify as u8, 3);
3597        assert_eq!(Alignment::Distribute as u8, 4);
3598        assert_eq!(Alignment::DistributeFlush as u8, 5);
3599    }
3600
3601    #[test]
3602    fn alignment_serde_roundtrip() {
3603        for variant in &[
3604            Alignment::Left,
3605            Alignment::Center,
3606            Alignment::Right,
3607            Alignment::Justify,
3608            Alignment::Distribute,
3609            Alignment::DistributeFlush,
3610        ] {
3611            let json = serde_json::to_string(variant).unwrap();
3612            let back: Alignment = serde_json::from_str(&json).unwrap();
3613            assert_eq!(&back, variant);
3614        }
3615    }
3616
3617    #[test]
3618    fn alignment_str_roundtrip() {
3619        for variant in &[
3620            Alignment::Left,
3621            Alignment::Center,
3622            Alignment::Right,
3623            Alignment::Justify,
3624            Alignment::Distribute,
3625            Alignment::DistributeFlush,
3626        ] {
3627            let s = variant.to_string();
3628            let back = Alignment::from_str(&s).unwrap();
3629            assert_eq!(&back, variant);
3630        }
3631    }
3632
3633    #[test]
3634    fn alignment_copy_and_hash() {
3635        use std::collections::HashSet;
3636        let a = Alignment::Left;
3637        let b = a; // Copy
3638        assert_eq!(a, b);
3639
3640        let mut set = HashSet::new();
3641        set.insert(Alignment::Left);
3642        set.insert(Alignment::Right);
3643        assert_eq!(set.len(), 2);
3644    }
3645
3646    // ===================================================================
3647    // LineSpacingType
3648    // ===================================================================
3649
3650    #[test]
3651    fn line_spacing_default_is_percentage() {
3652        assert_eq!(LineSpacingType::default(), LineSpacingType::Percentage);
3653    }
3654
3655    #[test]
3656    fn line_spacing_display() {
3657        assert_eq!(LineSpacingType::Percentage.to_string(), "Percentage");
3658        assert_eq!(LineSpacingType::Fixed.to_string(), "Fixed");
3659        assert_eq!(LineSpacingType::BetweenLines.to_string(), "BetweenLines");
3660    }
3661
3662    #[test]
3663    fn line_spacing_from_str() {
3664        assert_eq!(LineSpacingType::from_str("Percentage").unwrap(), LineSpacingType::Percentage);
3665        assert_eq!(LineSpacingType::from_str("Fixed").unwrap(), LineSpacingType::Fixed);
3666        assert_eq!(
3667            LineSpacingType::from_str("BetweenLines").unwrap(),
3668            LineSpacingType::BetweenLines
3669        );
3670        assert!(LineSpacingType::from_str("invalid").is_err());
3671    }
3672
3673    #[test]
3674    fn line_spacing_try_from_u8() {
3675        assert_eq!(LineSpacingType::try_from(0u8).unwrap(), LineSpacingType::Percentage);
3676        assert_eq!(LineSpacingType::try_from(1u8).unwrap(), LineSpacingType::Fixed);
3677        assert_eq!(LineSpacingType::try_from(2u8).unwrap(), LineSpacingType::BetweenLines);
3678        assert!(LineSpacingType::try_from(3u8).is_err());
3679    }
3680
3681    #[test]
3682    fn line_spacing_str_roundtrip() {
3683        for v in
3684            &[LineSpacingType::Percentage, LineSpacingType::Fixed, LineSpacingType::BetweenLines]
3685        {
3686            let s = v.to_string();
3687            let back = LineSpacingType::from_str(&s).unwrap();
3688            assert_eq!(&back, v);
3689        }
3690    }
3691
3692    // ===================================================================
3693    // BreakType
3694    // ===================================================================
3695
3696    #[test]
3697    fn break_type_default_is_none() {
3698        assert_eq!(BreakType::default(), BreakType::None);
3699    }
3700
3701    #[test]
3702    fn break_type_display() {
3703        assert_eq!(BreakType::None.to_string(), "None");
3704        assert_eq!(BreakType::Column.to_string(), "Column");
3705        assert_eq!(BreakType::Page.to_string(), "Page");
3706    }
3707
3708    #[test]
3709    fn break_type_from_str() {
3710        assert_eq!(BreakType::from_str("None").unwrap(), BreakType::None);
3711        assert_eq!(BreakType::from_str("Column").unwrap(), BreakType::Column);
3712        assert_eq!(BreakType::from_str("Page").unwrap(), BreakType::Page);
3713        assert!(BreakType::from_str("section").is_err());
3714    }
3715
3716    #[test]
3717    fn break_type_try_from_u8() {
3718        assert_eq!(BreakType::try_from(0u8).unwrap(), BreakType::None);
3719        assert_eq!(BreakType::try_from(1u8).unwrap(), BreakType::Column);
3720        assert_eq!(BreakType::try_from(2u8).unwrap(), BreakType::Page);
3721        assert!(BreakType::try_from(3u8).is_err());
3722    }
3723
3724    #[test]
3725    fn break_type_str_roundtrip() {
3726        for v in &[BreakType::None, BreakType::Column, BreakType::Page] {
3727            let s = v.to_string();
3728            let back = BreakType::from_str(&s).unwrap();
3729            assert_eq!(&back, v);
3730        }
3731    }
3732
3733    // ===================================================================
3734    // Language
3735    // ===================================================================
3736
3737    #[test]
3738    fn language_count_is_7() {
3739        assert_eq!(Language::COUNT, 7);
3740        assert_eq!(Language::ALL.len(), 7);
3741    }
3742
3743    #[test]
3744    fn language_default_is_korean() {
3745        assert_eq!(Language::default(), Language::Korean);
3746    }
3747
3748    #[test]
3749    fn language_discriminants() {
3750        assert_eq!(Language::Korean as u8, 0);
3751        assert_eq!(Language::English as u8, 1);
3752        assert_eq!(Language::Hanja as u8, 2);
3753        assert_eq!(Language::Japanese as u8, 3);
3754        assert_eq!(Language::Other as u8, 4);
3755        assert_eq!(Language::Symbol as u8, 5);
3756        assert_eq!(Language::User as u8, 6);
3757    }
3758
3759    #[test]
3760    fn language_display() {
3761        assert_eq!(Language::Korean.to_string(), "Korean");
3762        assert_eq!(Language::English.to_string(), "English");
3763        assert_eq!(Language::Japanese.to_string(), "Japanese");
3764    }
3765
3766    #[test]
3767    fn language_from_str() {
3768        for lang in &Language::ALL {
3769            let s = lang.to_string();
3770            let back = Language::from_str(&s).unwrap();
3771            assert_eq!(&back, lang);
3772        }
3773        assert!(Language::from_str("invalid").is_err());
3774    }
3775
3776    #[test]
3777    fn language_try_from_u8() {
3778        for (i, expected) in Language::ALL.iter().enumerate() {
3779            let parsed = Language::try_from(i as u8).unwrap();
3780            assert_eq!(&parsed, expected);
3781        }
3782        assert!(Language::try_from(7u8).is_err());
3783        assert!(Language::try_from(255u8).is_err());
3784    }
3785
3786    #[test]
3787    fn language_all_used_as_index() {
3788        // Common pattern: fonts[lang as usize]
3789        let fonts: [&str; Language::COUNT] =
3790            ["Batang", "Arial", "SimSun", "MS Mincho", "Arial", "Symbol", "Arial"];
3791        for lang in &Language::ALL {
3792            let _ = fonts[*lang as usize];
3793        }
3794    }
3795
3796    #[test]
3797    fn language_serde_roundtrip() {
3798        for lang in &Language::ALL {
3799            let json = serde_json::to_string(lang).unwrap();
3800            let back: Language = serde_json::from_str(&json).unwrap();
3801            assert_eq!(&back, lang);
3802        }
3803    }
3804
3805    // ===================================================================
3806    // UnderlineType
3807    // ===================================================================
3808
3809    #[test]
3810    fn underline_type_default_is_none() {
3811        assert_eq!(UnderlineType::default(), UnderlineType::None);
3812    }
3813
3814    #[test]
3815    fn underline_type_display() {
3816        assert_eq!(UnderlineType::None.to_string(), "None");
3817        assert_eq!(UnderlineType::Bottom.to_string(), "Bottom");
3818        assert_eq!(UnderlineType::Center.to_string(), "Center");
3819        assert_eq!(UnderlineType::Top.to_string(), "Top");
3820    }
3821
3822    #[test]
3823    fn underline_type_from_str() {
3824        assert_eq!(UnderlineType::from_str("None").unwrap(), UnderlineType::None);
3825        assert_eq!(UnderlineType::from_str("Bottom").unwrap(), UnderlineType::Bottom);
3826        assert_eq!(UnderlineType::from_str("center").unwrap(), UnderlineType::Center);
3827        assert!(UnderlineType::from_str("invalid").is_err());
3828    }
3829
3830    #[test]
3831    fn underline_type_try_from_u8() {
3832        assert_eq!(UnderlineType::try_from(0u8).unwrap(), UnderlineType::None);
3833        assert_eq!(UnderlineType::try_from(1u8).unwrap(), UnderlineType::Bottom);
3834        assert_eq!(UnderlineType::try_from(2u8).unwrap(), UnderlineType::Center);
3835        assert_eq!(UnderlineType::try_from(3u8).unwrap(), UnderlineType::Top);
3836        assert!(UnderlineType::try_from(4u8).is_err());
3837    }
3838
3839    #[test]
3840    fn underline_type_str_roundtrip() {
3841        for v in
3842            &[UnderlineType::None, UnderlineType::Bottom, UnderlineType::Center, UnderlineType::Top]
3843        {
3844            let s = v.to_string();
3845            let back = UnderlineType::from_str(&s).unwrap();
3846            assert_eq!(&back, v);
3847        }
3848    }
3849
3850    // ===================================================================
3851    // StrikeoutShape
3852    // ===================================================================
3853
3854    #[test]
3855    fn strikeout_shape_default_is_none() {
3856        assert_eq!(StrikeoutShape::default(), StrikeoutShape::None);
3857    }
3858
3859    #[test]
3860    fn strikeout_shape_display() {
3861        assert_eq!(StrikeoutShape::None.to_string(), "None");
3862        assert_eq!(StrikeoutShape::Continuous.to_string(), "Continuous");
3863        assert_eq!(StrikeoutShape::Dash.to_string(), "Dash");
3864        assert_eq!(StrikeoutShape::DashDotDot.to_string(), "DashDotDot");
3865    }
3866
3867    #[test]
3868    fn strikeout_shape_from_str() {
3869        assert_eq!(StrikeoutShape::from_str("None").unwrap(), StrikeoutShape::None);
3870        assert_eq!(StrikeoutShape::from_str("continuous").unwrap(), StrikeoutShape::Continuous);
3871        assert_eq!(StrikeoutShape::from_str("dash_dot").unwrap(), StrikeoutShape::DashDot);
3872        assert!(StrikeoutShape::from_str("invalid").is_err());
3873    }
3874
3875    #[test]
3876    fn strikeout_shape_try_from_u8() {
3877        assert_eq!(StrikeoutShape::try_from(0u8).unwrap(), StrikeoutShape::None);
3878        assert_eq!(StrikeoutShape::try_from(1u8).unwrap(), StrikeoutShape::Continuous);
3879        assert_eq!(StrikeoutShape::try_from(5u8).unwrap(), StrikeoutShape::DashDotDot);
3880        assert!(StrikeoutShape::try_from(6u8).is_err());
3881    }
3882
3883    #[test]
3884    fn strikeout_shape_str_roundtrip() {
3885        for v in &[
3886            StrikeoutShape::None,
3887            StrikeoutShape::Continuous,
3888            StrikeoutShape::Dash,
3889            StrikeoutShape::Dot,
3890            StrikeoutShape::DashDot,
3891            StrikeoutShape::DashDotDot,
3892        ] {
3893            let s = v.to_string();
3894            let back = StrikeoutShape::from_str(&s).unwrap();
3895            assert_eq!(&back, v);
3896        }
3897    }
3898
3899    // ===================================================================
3900    // OutlineType
3901    // ===================================================================
3902
3903    #[test]
3904    fn outline_type_default_is_none() {
3905        assert_eq!(OutlineType::default(), OutlineType::None);
3906    }
3907
3908    #[test]
3909    fn outline_type_display() {
3910        assert_eq!(OutlineType::None.to_string(), "None");
3911        assert_eq!(OutlineType::Solid.to_string(), "Solid");
3912    }
3913
3914    #[test]
3915    fn outline_type_from_str() {
3916        assert_eq!(OutlineType::from_str("None").unwrap(), OutlineType::None);
3917        assert_eq!(OutlineType::from_str("solid").unwrap(), OutlineType::Solid);
3918        assert!(OutlineType::from_str("dashed").is_err());
3919    }
3920
3921    #[test]
3922    fn outline_type_try_from_u8() {
3923        assert_eq!(OutlineType::try_from(0u8).unwrap(), OutlineType::None);
3924        assert_eq!(OutlineType::try_from(1u8).unwrap(), OutlineType::Solid);
3925        assert!(OutlineType::try_from(2u8).is_err());
3926    }
3927
3928    // ===================================================================
3929    // ShadowType
3930    // ===================================================================
3931
3932    #[test]
3933    fn shadow_type_default_is_none() {
3934        assert_eq!(ShadowType::default(), ShadowType::None);
3935    }
3936
3937    #[test]
3938    fn shadow_type_display() {
3939        assert_eq!(ShadowType::None.to_string(), "None");
3940        assert_eq!(ShadowType::Drop.to_string(), "Drop");
3941    }
3942
3943    #[test]
3944    fn shadow_type_from_str() {
3945        assert_eq!(ShadowType::from_str("None").unwrap(), ShadowType::None);
3946        assert_eq!(ShadowType::from_str("drop").unwrap(), ShadowType::Drop);
3947        assert!(ShadowType::from_str("shadow").is_err());
3948    }
3949
3950    #[test]
3951    fn shadow_type_try_from_u8() {
3952        assert_eq!(ShadowType::try_from(0u8).unwrap(), ShadowType::None);
3953        assert_eq!(ShadowType::try_from(1u8).unwrap(), ShadowType::Drop);
3954        assert!(ShadowType::try_from(2u8).is_err());
3955    }
3956
3957    // ===================================================================
3958    // EmbossType
3959    // ===================================================================
3960
3961    #[test]
3962    fn emboss_type_default_is_none() {
3963        assert_eq!(EmbossType::default(), EmbossType::None);
3964    }
3965
3966    #[test]
3967    fn emboss_type_display() {
3968        assert_eq!(EmbossType::None.to_string(), "None");
3969        assert_eq!(EmbossType::Emboss.to_string(), "Emboss");
3970    }
3971
3972    #[test]
3973    fn emboss_type_from_str() {
3974        assert_eq!(EmbossType::from_str("None").unwrap(), EmbossType::None);
3975        assert_eq!(EmbossType::from_str("emboss").unwrap(), EmbossType::Emboss);
3976        assert!(EmbossType::from_str("raised").is_err());
3977    }
3978
3979    #[test]
3980    fn emboss_type_try_from_u8() {
3981        assert_eq!(EmbossType::try_from(0u8).unwrap(), EmbossType::None);
3982        assert_eq!(EmbossType::try_from(1u8).unwrap(), EmbossType::Emboss);
3983        assert!(EmbossType::try_from(2u8).is_err());
3984    }
3985
3986    // ===================================================================
3987    // EngraveType
3988    // ===================================================================
3989
3990    #[test]
3991    fn engrave_type_default_is_none() {
3992        assert_eq!(EngraveType::default(), EngraveType::None);
3993    }
3994
3995    #[test]
3996    fn engrave_type_display() {
3997        assert_eq!(EngraveType::None.to_string(), "None");
3998        assert_eq!(EngraveType::Engrave.to_string(), "Engrave");
3999    }
4000
4001    #[test]
4002    fn engrave_type_from_str() {
4003        assert_eq!(EngraveType::from_str("None").unwrap(), EngraveType::None);
4004        assert_eq!(EngraveType::from_str("engrave").unwrap(), EngraveType::Engrave);
4005        assert!(EngraveType::from_str("sunken").is_err());
4006    }
4007
4008    #[test]
4009    fn engrave_type_try_from_u8() {
4010        assert_eq!(EngraveType::try_from(0u8).unwrap(), EngraveType::None);
4011        assert_eq!(EngraveType::try_from(1u8).unwrap(), EngraveType::Engrave);
4012        assert!(EngraveType::try_from(2u8).is_err());
4013    }
4014
4015    // ===================================================================
4016    // VerticalPosition
4017    // ===================================================================
4018
4019    #[test]
4020    fn vertical_position_default_is_normal() {
4021        assert_eq!(VerticalPosition::default(), VerticalPosition::Normal);
4022    }
4023
4024    #[test]
4025    fn vertical_position_display() {
4026        assert_eq!(VerticalPosition::Normal.to_string(), "Normal");
4027        assert_eq!(VerticalPosition::Superscript.to_string(), "Superscript");
4028        assert_eq!(VerticalPosition::Subscript.to_string(), "Subscript");
4029    }
4030
4031    #[test]
4032    fn vertical_position_from_str() {
4033        assert_eq!(VerticalPosition::from_str("Normal").unwrap(), VerticalPosition::Normal);
4034        assert_eq!(
4035            VerticalPosition::from_str("superscript").unwrap(),
4036            VerticalPosition::Superscript
4037        );
4038        assert_eq!(VerticalPosition::from_str("sub").unwrap(), VerticalPosition::Subscript);
4039        assert!(VerticalPosition::from_str("middle").is_err());
4040    }
4041
4042    #[test]
4043    fn vertical_position_try_from_u8() {
4044        assert_eq!(VerticalPosition::try_from(0u8).unwrap(), VerticalPosition::Normal);
4045        assert_eq!(VerticalPosition::try_from(1u8).unwrap(), VerticalPosition::Superscript);
4046        assert_eq!(VerticalPosition::try_from(2u8).unwrap(), VerticalPosition::Subscript);
4047        assert!(VerticalPosition::try_from(3u8).is_err());
4048    }
4049
4050    #[test]
4051    fn vertical_position_str_roundtrip() {
4052        for v in
4053            &[VerticalPosition::Normal, VerticalPosition::Superscript, VerticalPosition::Subscript]
4054        {
4055            let s = v.to_string();
4056            let back = VerticalPosition::from_str(&s).unwrap();
4057            assert_eq!(&back, v);
4058        }
4059    }
4060
4061    // ===================================================================
4062    // BorderLineType
4063    // ===================================================================
4064
4065    #[test]
4066    fn border_line_type_default_is_none() {
4067        assert_eq!(BorderLineType::default(), BorderLineType::None);
4068    }
4069
4070    #[test]
4071    fn border_line_type_display() {
4072        assert_eq!(BorderLineType::None.to_string(), "None");
4073        assert_eq!(BorderLineType::Solid.to_string(), "Solid");
4074        assert_eq!(BorderLineType::DashDot.to_string(), "DashDot");
4075        assert_eq!(BorderLineType::ThickBetweenSlim.to_string(), "ThickBetweenSlim");
4076    }
4077
4078    #[test]
4079    fn border_line_type_from_str() {
4080        assert_eq!(BorderLineType::from_str("None").unwrap(), BorderLineType::None);
4081        assert_eq!(BorderLineType::from_str("solid").unwrap(), BorderLineType::Solid);
4082        assert_eq!(BorderLineType::from_str("dash_dot").unwrap(), BorderLineType::DashDot);
4083        assert_eq!(BorderLineType::from_str("double").unwrap(), BorderLineType::Double);
4084        assert!(BorderLineType::from_str("wavy").is_err());
4085    }
4086
4087    #[test]
4088    fn border_line_type_try_from_u8() {
4089        assert_eq!(BorderLineType::try_from(0u8).unwrap(), BorderLineType::None);
4090        assert_eq!(BorderLineType::try_from(1u8).unwrap(), BorderLineType::Solid);
4091        assert_eq!(BorderLineType::try_from(10u8).unwrap(), BorderLineType::ThickBetweenSlim);
4092        assert!(BorderLineType::try_from(11u8).is_err());
4093    }
4094
4095    #[test]
4096    fn border_line_type_str_roundtrip() {
4097        for v in &[
4098            BorderLineType::None,
4099            BorderLineType::Solid,
4100            BorderLineType::Dash,
4101            BorderLineType::Dot,
4102            BorderLineType::DashDot,
4103            BorderLineType::DashDotDot,
4104            BorderLineType::LongDash,
4105            BorderLineType::TripleDot,
4106            BorderLineType::Double,
4107            BorderLineType::DoubleSlim,
4108            BorderLineType::ThickBetweenSlim,
4109        ] {
4110            let s = v.to_string();
4111            let back = BorderLineType::from_str(&s).unwrap();
4112            assert_eq!(&back, v);
4113        }
4114    }
4115
4116    // ===================================================================
4117    // FillBrushType
4118    // ===================================================================
4119
4120    #[test]
4121    fn fill_brush_type_default_is_none() {
4122        assert_eq!(FillBrushType::default(), FillBrushType::None);
4123    }
4124
4125    #[test]
4126    fn fill_brush_type_display() {
4127        assert_eq!(FillBrushType::None.to_string(), "None");
4128        assert_eq!(FillBrushType::Solid.to_string(), "Solid");
4129        assert_eq!(FillBrushType::Gradient.to_string(), "Gradient");
4130        assert_eq!(FillBrushType::Pattern.to_string(), "Pattern");
4131    }
4132
4133    #[test]
4134    fn fill_brush_type_from_str() {
4135        assert_eq!(FillBrushType::from_str("None").unwrap(), FillBrushType::None);
4136        assert_eq!(FillBrushType::from_str("solid").unwrap(), FillBrushType::Solid);
4137        assert_eq!(FillBrushType::from_str("gradient").unwrap(), FillBrushType::Gradient);
4138        assert!(FillBrushType::from_str("texture").is_err());
4139    }
4140
4141    #[test]
4142    fn fill_brush_type_try_from_u8() {
4143        assert_eq!(FillBrushType::try_from(0u8).unwrap(), FillBrushType::None);
4144        assert_eq!(FillBrushType::try_from(1u8).unwrap(), FillBrushType::Solid);
4145        assert_eq!(FillBrushType::try_from(2u8).unwrap(), FillBrushType::Gradient);
4146        assert_eq!(FillBrushType::try_from(3u8).unwrap(), FillBrushType::Pattern);
4147        assert!(FillBrushType::try_from(4u8).is_err());
4148    }
4149
4150    #[test]
4151    fn fill_brush_type_str_roundtrip() {
4152        for v in &[
4153            FillBrushType::None,
4154            FillBrushType::Solid,
4155            FillBrushType::Gradient,
4156            FillBrushType::Pattern,
4157        ] {
4158            let s = v.to_string();
4159            let back = FillBrushType::from_str(&s).unwrap();
4160            assert_eq!(&back, v);
4161        }
4162    }
4163
4164    // ===================================================================
4165    // Cross-enum size assertions (compile-time already, but test at runtime too)
4166    // ===================================================================
4167
4168    #[test]
4169    fn all_enums_are_one_byte() {
4170        assert_eq!(std::mem::size_of::<Alignment>(), 1);
4171        assert_eq!(std::mem::size_of::<LineSpacingType>(), 1);
4172        assert_eq!(std::mem::size_of::<BreakType>(), 1);
4173        assert_eq!(std::mem::size_of::<Language>(), 1);
4174        assert_eq!(std::mem::size_of::<UnderlineType>(), 1);
4175        assert_eq!(std::mem::size_of::<StrikeoutShape>(), 1);
4176        assert_eq!(std::mem::size_of::<OutlineType>(), 1);
4177        assert_eq!(std::mem::size_of::<ShadowType>(), 1);
4178        assert_eq!(std::mem::size_of::<EmbossType>(), 1);
4179        assert_eq!(std::mem::size_of::<EngraveType>(), 1);
4180        assert_eq!(std::mem::size_of::<VerticalPosition>(), 1);
4181        assert_eq!(std::mem::size_of::<BorderLineType>(), 1);
4182        assert_eq!(std::mem::size_of::<FillBrushType>(), 1);
4183        assert_eq!(std::mem::size_of::<ApplyPageType>(), 1);
4184        assert_eq!(std::mem::size_of::<NumberFormatType>(), 1);
4185        assert_eq!(std::mem::size_of::<PageNumberPosition>(), 1);
4186    }
4187
4188    // ===================================================================
4189    // ApplyPageType
4190    // ===================================================================
4191
4192    #[test]
4193    fn apply_page_type_default_is_both() {
4194        assert_eq!(ApplyPageType::default(), ApplyPageType::Both);
4195    }
4196
4197    #[test]
4198    fn apply_page_type_display() {
4199        assert_eq!(ApplyPageType::Both.to_string(), "Both");
4200        assert_eq!(ApplyPageType::Even.to_string(), "Even");
4201        assert_eq!(ApplyPageType::Odd.to_string(), "Odd");
4202    }
4203
4204    #[test]
4205    fn apply_page_type_from_str() {
4206        assert_eq!(ApplyPageType::from_str("Both").unwrap(), ApplyPageType::Both);
4207        assert_eq!(ApplyPageType::from_str("BOTH").unwrap(), ApplyPageType::Both);
4208        assert_eq!(ApplyPageType::from_str("even").unwrap(), ApplyPageType::Even);
4209        assert_eq!(ApplyPageType::from_str("ODD").unwrap(), ApplyPageType::Odd);
4210        assert!(ApplyPageType::from_str("invalid").is_err());
4211    }
4212
4213    #[test]
4214    fn apply_page_type_try_from_u8() {
4215        assert_eq!(ApplyPageType::try_from(0u8).unwrap(), ApplyPageType::Both);
4216        assert_eq!(ApplyPageType::try_from(1u8).unwrap(), ApplyPageType::Even);
4217        assert_eq!(ApplyPageType::try_from(2u8).unwrap(), ApplyPageType::Odd);
4218        assert!(ApplyPageType::try_from(3u8).is_err());
4219    }
4220
4221    #[test]
4222    fn apply_page_type_str_roundtrip() {
4223        for v in &[ApplyPageType::Both, ApplyPageType::Even, ApplyPageType::Odd] {
4224            let s = v.to_string();
4225            let back = ApplyPageType::from_str(&s).unwrap();
4226            assert_eq!(&back, v);
4227        }
4228    }
4229
4230    // ===================================================================
4231    // NumberFormatType
4232    // ===================================================================
4233
4234    #[test]
4235    fn number_format_type_default_is_digit() {
4236        assert_eq!(NumberFormatType::default(), NumberFormatType::Digit);
4237    }
4238
4239    #[test]
4240    fn number_format_type_display() {
4241        assert_eq!(NumberFormatType::Digit.to_string(), "Digit");
4242        assert_eq!(NumberFormatType::CircledDigit.to_string(), "CircledDigit");
4243        assert_eq!(NumberFormatType::RomanCapital.to_string(), "RomanCapital");
4244        assert_eq!(NumberFormatType::HanjaDigit.to_string(), "HanjaDigit");
4245    }
4246
4247    #[test]
4248    fn number_format_type_from_str() {
4249        assert_eq!(NumberFormatType::from_str("Digit").unwrap(), NumberFormatType::Digit);
4250        assert_eq!(NumberFormatType::from_str("DIGIT").unwrap(), NumberFormatType::Digit);
4251        assert_eq!(
4252            NumberFormatType::from_str("CircledDigit").unwrap(),
4253            NumberFormatType::CircledDigit
4254        );
4255        assert_eq!(
4256            NumberFormatType::from_str("ROMAN_CAPITAL").unwrap(),
4257            NumberFormatType::RomanCapital
4258        );
4259        assert!(NumberFormatType::from_str("invalid").is_err());
4260    }
4261
4262    #[test]
4263    fn number_format_type_try_from_u8() {
4264        assert_eq!(NumberFormatType::try_from(0u8).unwrap(), NumberFormatType::Digit);
4265        assert_eq!(NumberFormatType::try_from(1u8).unwrap(), NumberFormatType::CircledDigit);
4266        assert_eq!(NumberFormatType::try_from(8u8).unwrap(), NumberFormatType::HanjaDigit);
4267        assert_eq!(
4268            NumberFormatType::try_from(9u8).unwrap(),
4269            NumberFormatType::CircledHangulSyllable
4270        );
4271        assert!(NumberFormatType::try_from(10u8).is_err());
4272    }
4273
4274    #[test]
4275    fn number_format_type_circled_hangul_syllable() {
4276        assert_eq!(NumberFormatType::CircledHangulSyllable.to_string(), "CircledHangulSyllable");
4277        assert_eq!(
4278            NumberFormatType::from_str("CircledHangulSyllable").unwrap(),
4279            NumberFormatType::CircledHangulSyllable
4280        );
4281        assert_eq!(
4282            NumberFormatType::from_str("CIRCLED_HANGUL_SYLLABLE").unwrap(),
4283            NumberFormatType::CircledHangulSyllable
4284        );
4285    }
4286
4287    #[test]
4288    fn number_format_type_str_roundtrip() {
4289        for v in &[
4290            NumberFormatType::Digit,
4291            NumberFormatType::CircledDigit,
4292            NumberFormatType::RomanCapital,
4293            NumberFormatType::RomanSmall,
4294            NumberFormatType::LatinCapital,
4295            NumberFormatType::LatinSmall,
4296            NumberFormatType::HangulSyllable,
4297            NumberFormatType::HangulJamo,
4298            NumberFormatType::HanjaDigit,
4299            NumberFormatType::CircledHangulSyllable,
4300        ] {
4301            let s = v.to_string();
4302            let back = NumberFormatType::from_str(&s).unwrap();
4303            assert_eq!(&back, v);
4304        }
4305    }
4306
4307    // ===================================================================
4308    // PageNumberPosition
4309    // ===================================================================
4310
4311    #[test]
4312    fn page_number_position_default_is_top_center() {
4313        assert_eq!(PageNumberPosition::default(), PageNumberPosition::TopCenter);
4314    }
4315
4316    #[test]
4317    fn page_number_position_display() {
4318        assert_eq!(PageNumberPosition::None.to_string(), "None");
4319        assert_eq!(PageNumberPosition::TopCenter.to_string(), "TopCenter");
4320        assert_eq!(PageNumberPosition::BottomCenter.to_string(), "BottomCenter");
4321        assert_eq!(PageNumberPosition::InsideBottom.to_string(), "InsideBottom");
4322    }
4323
4324    #[test]
4325    fn page_number_position_from_str() {
4326        assert_eq!(PageNumberPosition::from_str("None").unwrap(), PageNumberPosition::None);
4327        assert_eq!(
4328            PageNumberPosition::from_str("BOTTOM_CENTER").unwrap(),
4329            PageNumberPosition::BottomCenter
4330        );
4331        assert_eq!(
4332            PageNumberPosition::from_str("bottom-center").unwrap(),
4333            PageNumberPosition::BottomCenter
4334        );
4335        assert_eq!(PageNumberPosition::from_str("TopLeft").unwrap(), PageNumberPosition::TopLeft);
4336        assert!(PageNumberPosition::from_str("invalid").is_err());
4337    }
4338
4339    #[test]
4340    fn page_number_position_try_from_u8() {
4341        assert_eq!(PageNumberPosition::try_from(0u8).unwrap(), PageNumberPosition::None);
4342        assert_eq!(PageNumberPosition::try_from(2u8).unwrap(), PageNumberPosition::TopCenter);
4343        assert_eq!(PageNumberPosition::try_from(5u8).unwrap(), PageNumberPosition::BottomCenter);
4344        assert_eq!(PageNumberPosition::try_from(10u8).unwrap(), PageNumberPosition::InsideBottom);
4345        assert!(PageNumberPosition::try_from(11u8).is_err());
4346    }
4347
4348    #[test]
4349    fn page_number_position_str_roundtrip() {
4350        for v in &[
4351            PageNumberPosition::None,
4352            PageNumberPosition::TopLeft,
4353            PageNumberPosition::TopCenter,
4354            PageNumberPosition::TopRight,
4355            PageNumberPosition::BottomLeft,
4356            PageNumberPosition::BottomCenter,
4357            PageNumberPosition::BottomRight,
4358            PageNumberPosition::OutsideTop,
4359            PageNumberPosition::OutsideBottom,
4360            PageNumberPosition::InsideTop,
4361            PageNumberPosition::InsideBottom,
4362        ] {
4363            let s = v.to_string();
4364            let back = PageNumberPosition::from_str(&s).unwrap();
4365            assert_eq!(&back, v);
4366        }
4367    }
4368
4369    // ===================================================================
4370    // WordBreakType
4371    // ===================================================================
4372
4373    #[test]
4374    fn word_break_type_default_is_keep_word() {
4375        assert_eq!(WordBreakType::default(), WordBreakType::KeepWord);
4376    }
4377
4378    #[test]
4379    fn word_break_type_display() {
4380        assert_eq!(WordBreakType::KeepWord.to_string(), "KEEP_WORD");
4381        assert_eq!(WordBreakType::BreakWord.to_string(), "BREAK_WORD");
4382    }
4383
4384    #[test]
4385    fn word_break_type_from_str() {
4386        assert_eq!(WordBreakType::from_str("KEEP_WORD").unwrap(), WordBreakType::KeepWord);
4387        assert_eq!(WordBreakType::from_str("KeepWord").unwrap(), WordBreakType::KeepWord);
4388        assert_eq!(WordBreakType::from_str("keep_word").unwrap(), WordBreakType::KeepWord);
4389        assert_eq!(WordBreakType::from_str("BREAK_WORD").unwrap(), WordBreakType::BreakWord);
4390        assert_eq!(WordBreakType::from_str("BreakWord").unwrap(), WordBreakType::BreakWord);
4391        assert_eq!(WordBreakType::from_str("break_word").unwrap(), WordBreakType::BreakWord);
4392        assert!(WordBreakType::from_str("invalid").is_err());
4393    }
4394
4395    #[test]
4396    fn word_break_type_try_from_u8() {
4397        assert_eq!(WordBreakType::try_from(0u8).unwrap(), WordBreakType::KeepWord);
4398        assert_eq!(WordBreakType::try_from(1u8).unwrap(), WordBreakType::BreakWord);
4399        assert!(WordBreakType::try_from(2u8).is_err());
4400    }
4401
4402    #[test]
4403    fn word_break_type_serde_roundtrip() {
4404        for v in &[WordBreakType::KeepWord, WordBreakType::BreakWord] {
4405            let json = serde_json::to_string(v).unwrap();
4406            let back: WordBreakType = serde_json::from_str(&json).unwrap();
4407            assert_eq!(&back, v);
4408        }
4409    }
4410
4411    #[test]
4412    fn word_break_type_str_roundtrip() {
4413        for v in &[WordBreakType::KeepWord, WordBreakType::BreakWord] {
4414            let s = v.to_string();
4415            let back = WordBreakType::from_str(&s).unwrap();
4416            assert_eq!(&back, v);
4417        }
4418    }
4419
4420    // ===================================================================
4421    // EmphasisType
4422    // ===================================================================
4423
4424    #[test]
4425    fn emphasis_type_default_is_none() {
4426        assert_eq!(EmphasisType::default(), EmphasisType::None);
4427    }
4428
4429    #[test]
4430    fn emphasis_type_display_pascal_case() {
4431        assert_eq!(EmphasisType::None.to_string(), "None");
4432        assert_eq!(EmphasisType::DotAbove.to_string(), "DotAbove");
4433        assert_eq!(EmphasisType::RingAbove.to_string(), "RingAbove");
4434        assert_eq!(EmphasisType::Tilde.to_string(), "Tilde");
4435        assert_eq!(EmphasisType::Caron.to_string(), "Caron");
4436        assert_eq!(EmphasisType::Side.to_string(), "Side");
4437        assert_eq!(EmphasisType::Colon.to_string(), "Colon");
4438        assert_eq!(EmphasisType::GraveAccent.to_string(), "GraveAccent");
4439        assert_eq!(EmphasisType::AcuteAccent.to_string(), "AcuteAccent");
4440        assert_eq!(EmphasisType::Circumflex.to_string(), "Circumflex");
4441        assert_eq!(EmphasisType::Macron.to_string(), "Macron");
4442        assert_eq!(EmphasisType::HookAbove.to_string(), "HookAbove");
4443        assert_eq!(EmphasisType::DotBelow.to_string(), "DotBelow");
4444    }
4445
4446    #[test]
4447    fn emphasis_type_from_str_screaming_snake_case() {
4448        assert_eq!(EmphasisType::from_str("NONE").unwrap(), EmphasisType::None);
4449        assert_eq!(EmphasisType::from_str("DOT_ABOVE").unwrap(), EmphasisType::DotAbove);
4450        assert_eq!(EmphasisType::from_str("RING_ABOVE").unwrap(), EmphasisType::RingAbove);
4451        assert_eq!(EmphasisType::from_str("GRAVE_ACCENT").unwrap(), EmphasisType::GraveAccent);
4452        assert_eq!(EmphasisType::from_str("DOT_BELOW").unwrap(), EmphasisType::DotBelow);
4453    }
4454
4455    #[test]
4456    fn emphasis_type_from_str_pascal_case() {
4457        assert_eq!(EmphasisType::from_str("None").unwrap(), EmphasisType::None);
4458        assert_eq!(EmphasisType::from_str("DotAbove").unwrap(), EmphasisType::DotAbove);
4459        assert_eq!(EmphasisType::from_str("HookAbove").unwrap(), EmphasisType::HookAbove);
4460    }
4461
4462    #[test]
4463    fn emphasis_type_from_str_invalid() {
4464        let err = EmphasisType::from_str("INVALID").unwrap_err();
4465        match err {
4466            FoundationError::ParseError { ref type_name, ref value, .. } => {
4467                assert_eq!(type_name, "EmphasisType");
4468                assert_eq!(value, "INVALID");
4469            }
4470            other => panic!("unexpected: {other}"),
4471        }
4472    }
4473
4474    #[test]
4475    fn emphasis_type_try_from_u8() {
4476        assert_eq!(EmphasisType::try_from(0u8).unwrap(), EmphasisType::None);
4477        assert_eq!(EmphasisType::try_from(1u8).unwrap(), EmphasisType::DotAbove);
4478        assert_eq!(EmphasisType::try_from(12u8).unwrap(), EmphasisType::DotBelow);
4479        assert!(EmphasisType::try_from(13u8).is_err());
4480        assert!(EmphasisType::try_from(255u8).is_err());
4481    }
4482
4483    #[test]
4484    fn emphasis_type_repr_values() {
4485        assert_eq!(EmphasisType::None as u8, 0);
4486        assert_eq!(EmphasisType::DotAbove as u8, 1);
4487        assert_eq!(EmphasisType::DotBelow as u8, 12);
4488    }
4489
4490    #[test]
4491    fn emphasis_type_serde_roundtrip() {
4492        for variant in &[
4493            EmphasisType::None,
4494            EmphasisType::DotAbove,
4495            EmphasisType::RingAbove,
4496            EmphasisType::DotBelow,
4497        ] {
4498            let json = serde_json::to_string(variant).unwrap();
4499            let back: EmphasisType = serde_json::from_str(&json).unwrap();
4500            assert_eq!(&back, variant);
4501        }
4502    }
4503
4504    #[test]
4505    fn emphasis_type_str_roundtrip() {
4506        for variant in &[
4507            EmphasisType::None,
4508            EmphasisType::DotAbove,
4509            EmphasisType::GraveAccent,
4510            EmphasisType::DotBelow,
4511        ] {
4512            let s = variant.to_string();
4513            let back = EmphasisType::from_str(&s).unwrap();
4514            assert_eq!(&back, variant);
4515        }
4516    }
4517}