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