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 (누름틀) or SUMMERY auto-field carried in
3277/// [`Control::Field`].
3278///
3279/// `ClickHere` is the press-field (user-fillable form). The rest are
3280/// SUMMERY-family auto-fields (HWPX `<hp:fieldBegin type="SUMMERY">`,
3281/// HWP5 `%smr` ctrl_id) that resolve to document metadata at render time.
3282///
3283/// Wave 12n (2026-06-02) breaking: legacy `Date`/`Time`/`DocSummary`/`UserInfo`
3284/// variants were renamed to match the actual HWPX `Command` token and 한컴
3285/// menu item, and `PageNum` was moved out to [`Control::InlinePageNumber`]
3286/// because the wire family is `atno`, not SUMMERY/fieldBegin.
3287#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
3288#[non_exhaustive]
3289#[repr(u8)]
3290pub enum FieldType {
3291    /// Click-here placeholder press-field (default). 한컴 누름틀. HWP5 `%clk`.
3292    #[default]
3293    ClickHere = 0,
3294    /// SUMMERY `$author` — 만든 사람 (was [`FieldType::DocSummary`]).
3295    Author = 1,
3296    /// SUMMERY `$lastsaveby` — 마지막 저장한 사람 (was [`FieldType::UserInfo`]).
3297    LastSavedBy = 2,
3298    /// SUMMERY `$createtime` — 만든 날짜 (was [`FieldType::Time`]).
3299    CreatedTime = 3,
3300    /// SUMMERY `$modifiedtime` — 마지막 저장한 날짜 (was [`FieldType::Date`]).
3301    ModifiedTime = 4,
3302    /// SUMMERY `$title` — 문서 제목 (NEW in Wave 12n).
3303    Title = 5,
3304}
3305
3306impl FieldType {
3307    /// Returns the HWPX `Command` `$token` for SUMMERY variants, or `None`
3308    /// for non-SUMMERY (`ClickHere`).
3309    #[must_use]
3310    pub fn summery_token(self) -> Option<&'static str> {
3311        match self {
3312            Self::ClickHere => None,
3313            Self::Author => Some("$author"),
3314            Self::LastSavedBy => Some("$lastsaveby"),
3315            Self::CreatedTime => Some("$createtime"),
3316            Self::ModifiedTime => Some("$modifiedtime"),
3317            Self::Title => Some("$title"),
3318        }
3319    }
3320
3321    /// Parses a SUMMERY `$token` (`$author`, `$createtime`, …) into a
3322    /// matching [`FieldType`]. Returns `None` for unknown tokens; callers
3323    /// should carry the raw token via [`Control::UnknownSummery`] instead.
3324    #[must_use]
3325    pub fn from_summery_token(token: &str) -> Option<Self> {
3326        match token {
3327            "$author" => Some(Self::Author),
3328            "$lastsaveby" => Some(Self::LastSavedBy),
3329            "$createtime" => Some(Self::CreatedTime),
3330            "$modifiedtime" => Some(Self::ModifiedTime),
3331            "$title" => Some(Self::Title),
3332            _ => None,
3333        }
3334    }
3335
3336    /// Returns the HWPX `<hp:fieldBegin editable="…">` value for this
3337    /// field type (Wave 12p task #124).
3338    ///
3339    /// Empirically derived from the Hancom-native
3340    /// `sample-field-docsummary.hwpx`:
3341    /// - `Author`, `Title` → `false` (stable user metadata; Hancom keeps
3342    ///   the authored value)
3343    /// - `LastSavedBy`, `CreatedTime`, `ModifiedTime` → `true` (Hancom
3344    ///   recomputes from the document metadata on save)
3345    /// - `ClickHere` → `true` (placeholder press-fields are user-editable)
3346    ///
3347    /// Encoders use this method to populate the `editable` attribute
3348    /// instead of the prior hardcoded `"1"` (which previously caused
3349    /// `$author`/`$title` to appear edited when re-saved by Hancom).
3350    #[must_use]
3351    pub fn hwpx_editable(self) -> bool {
3352        match self {
3353            Self::ClickHere => true,
3354            Self::Author | Self::Title => false,
3355            Self::LastSavedBy | Self::CreatedTime | Self::ModifiedTime => true,
3356        }
3357    }
3358}
3359
3360impl fmt::Display for FieldType {
3361    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3362        match self {
3363            Self::ClickHere => f.write_str("CLICK_HERE"),
3364            Self::Author => f.write_str("AUTHOR"),
3365            Self::LastSavedBy => f.write_str("LAST_SAVED_BY"),
3366            Self::CreatedTime => f.write_str("CREATED_TIME"),
3367            Self::ModifiedTime => f.write_str("MODIFIED_TIME"),
3368            Self::Title => f.write_str("TITLE"),
3369        }
3370    }
3371}
3372
3373impl std::str::FromStr for FieldType {
3374    type Err = FoundationError;
3375
3376    fn from_str(s: &str) -> Result<Self, Self::Err> {
3377        match s {
3378            "CLICK_HERE" | "ClickHere" | "click_here" => Ok(Self::ClickHere),
3379            "AUTHOR" | "Author" | "author"
3380            // legacy Wave 12l alias: DocSummary used to emit $author
3381            | "DOC_SUMMARY" | "DocSummary" | "doc_summary" => Ok(Self::Author),
3382            "LAST_SAVED_BY" | "LastSavedBy" | "last_saved_by"
3383            // legacy alias: UserInfo used to emit $lastsaveby
3384            | "USER_INFO" | "UserInfo" | "user_info" => Ok(Self::LastSavedBy),
3385            "CREATED_TIME" | "CreatedTime" | "created_time"
3386            // legacy alias: Time used to emit $createtime
3387            | "TIME" | "Time" | "time" => Ok(Self::CreatedTime),
3388            "MODIFIED_TIME" | "ModifiedTime" | "modified_time"
3389            // legacy alias: Date used to emit $modifiedtime
3390            | "DATE" | "Date" | "date" => Ok(Self::ModifiedTime),
3391            "TITLE" | "Title" | "title" => Ok(Self::Title),
3392            _ => Err(FoundationError::ParseError {
3393                type_name: "FieldType".to_string(),
3394                value: s.to_string(),
3395                valid_values:
3396                    "CLICK_HERE, AUTHOR, LAST_SAVED_BY, CREATED_TIME, MODIFIED_TIME, TITLE"
3397                        .to_string(),
3398            }),
3399        }
3400    }
3401}
3402
3403impl TryFrom<u8> for FieldType {
3404    type Error = FoundationError;
3405
3406    fn try_from(value: u8) -> Result<Self, Self::Error> {
3407        match value {
3408            0 => Ok(Self::ClickHere),
3409            1 => Ok(Self::Author),
3410            2 => Ok(Self::LastSavedBy),
3411            3 => Ok(Self::CreatedTime),
3412            4 => Ok(Self::ModifiedTime),
3413            5 => Ok(Self::Title),
3414            _ => Err(FoundationError::ParseError {
3415                type_name: "FieldType".to_string(),
3416                value: value.to_string(),
3417                valid_values: "0..5 (ClickHere..Title)".to_string(),
3418            }),
3419        }
3420    }
3421}
3422
3423impl schemars::JsonSchema for FieldType {
3424    fn schema_name() -> std::borrow::Cow<'static, str> {
3425        std::borrow::Cow::Borrowed("FieldType")
3426    }
3427
3428    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3429        gen.subschema_for::<String>()
3430    }
3431}
3432
3433// ---------------------------------------------------------------------------
3434// RefType
3435// ---------------------------------------------------------------------------
3436
3437/// Target type of a cross-reference (상호참조).
3438///
3439/// Wave 12m Phase 2 (Codex(architect+critic) review 반영):
3440/// - `#[repr(u8)]` 제거 — HWP5 wire 코드는 boundary 에서 매핑 (Core/foundation
3441///   은 wire-agnostic 의미 모델만 유지).
3442/// - `Footnote` / `Endnote` / `Outline` 추가 (한컴 native dropdown 7 종 완비).
3443/// - `Unknown(u8)` tuple variant 추가 — silent fallback 위조 방지 (Codex HIGH).
3444///
3445/// HWP5 `%xrf` Command N1 코드 → `RefType` 변환은
3446/// `smithy-hwp5/src/projection.rs::decode_hwp5_ref_type` 에서 수행.
3447#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
3448#[non_exhaustive]
3449pub enum RefType {
3450    /// Reference to a bookmark target. 한컴: "책갈피"
3451    #[default]
3452    Bookmark,
3453    /// Reference to a table caption number. 한컴: "표"
3454    Table,
3455    /// Reference to a figure/image caption number. 한컴: "그림"
3456    Figure,
3457    /// Reference to an equation caption number. 한컴: "수식"
3458    Equation,
3459    /// Reference to a footnote (각주).
3460    Footnote,
3461    /// Reference to an endnote (미주).
3462    Endnote,
3463    /// Reference to an outline heading (개요).
3464    Outline,
3465    /// Unrecognized RefType code preserved from wire for forward
3466    /// compatibility. Carriers MUST NOT silently fallback to a known
3467    /// variant — the unknown code is preserved so future Hancom
3468    /// extensions don't get silently lost.
3469    Unknown(u8),
3470}
3471
3472impl fmt::Display for RefType {
3473    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3474        match self {
3475            Self::Bookmark => f.write_str("TARGET_BOOKMARK"),
3476            Self::Table => f.write_str("TARGET_TABLE"),
3477            Self::Figure => f.write_str("TARGET_FIGURE"),
3478            Self::Equation => f.write_str("TARGET_EQUATION"),
3479            Self::Footnote => f.write_str("TARGET_FOOTNOTE"),
3480            Self::Endnote => f.write_str("TARGET_ENDNOTE"),
3481            Self::Outline => f.write_str("TARGET_OUTLINE"),
3482            Self::Unknown(code) => write!(f, "TARGET_UNKNOWN({code})"),
3483        }
3484    }
3485}
3486
3487impl std::str::FromStr for RefType {
3488    type Err = FoundationError;
3489
3490    fn from_str(s: &str) -> Result<Self, Self::Err> {
3491        match s {
3492            "TARGET_BOOKMARK" | "Bookmark" | "bookmark" => Ok(Self::Bookmark),
3493            "TARGET_TABLE" | "Table" | "table" => Ok(Self::Table),
3494            "TARGET_FIGURE" | "Figure" | "figure" => Ok(Self::Figure),
3495            "TARGET_EQUATION" | "Equation" | "equation" => Ok(Self::Equation),
3496            "TARGET_FOOTNOTE" | "Footnote" | "footnote" => Ok(Self::Footnote),
3497            "TARGET_ENDNOTE" | "Endnote" | "endnote" => Ok(Self::Endnote),
3498            "TARGET_OUTLINE" | "Outline" | "outline" => Ok(Self::Outline),
3499            _ => Err(FoundationError::ParseError {
3500                type_name: "RefType".to_string(),
3501                value: s.to_string(),
3502                valid_values: "TARGET_BOOKMARK, TARGET_TABLE, TARGET_FIGURE, TARGET_EQUATION, \
3503                    TARGET_FOOTNOTE, TARGET_ENDNOTE, TARGET_OUTLINE"
3504                    .to_string(),
3505            }),
3506        }
3507    }
3508}
3509
3510impl schemars::JsonSchema for RefType {
3511    fn schema_name() -> std::borrow::Cow<'static, str> {
3512        std::borrow::Cow::Borrowed("RefType")
3513    }
3514
3515    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3516        gen.subschema_for::<String>()
3517    }
3518}
3519
3520// ---------------------------------------------------------------------------
3521// RefContentType
3522// ---------------------------------------------------------------------------
3523
3524/// Content display type for a cross-reference (what to show at the reference site).
3525///
3526/// Wave 12m Phase 2 (Codex(architect+critic) review 반영):
3527/// - `#[repr(u8)]` 제거 — N2 wire 코드는 boundary 에서 매핑.
3528/// - `Unknown(u8)` 추가 — silent fallback 위조 방지.
3529///
3530/// Wave 12p pre-fix (Wave 12m fixup regression revert + 한컴 native
3531/// wire 일치 보정, 2026-06-08):
3532///
3533/// Wave 12m fixup 에서 `BookmarkName` 폐기 + Bookmark+Contents 를 N2=2
3534/// 로 통일했으나, native wire 분석 결과 정반대였음:
3535///
3536/// | HWP5 N2 (Bookmark) | 한컴 의미     | HWPX RefContentType 문자열 |
3537/// |--------------------|---------------|---------------------------|
3538/// | 0                  | 책갈피 위치 페이지 | `OBJECT_TYPE_PAGE`        |
3539/// | 1                  | 책갈피 본문 / 번호 | `OBJECT_TYPE_NUMBER` (!!) |
3540/// | 2                  | 책갈피 이름       | `OBJECT_TYPE_CONTENTS`    |
3541/// | 3                  | 위/아래          | `OBJECT_TYPE_UPDOWNPOS`   |
3542///
3543/// (책갈피의 wire 는 OWPML spec 표 156 의 직관과 어긋남 — `CONTENTS`
3544/// 가 "책갈피 이름", `NUMBER` 가 "책갈피 내용" 의미.) `BookmarkName`
3545/// variant 부활 + Display 는 `OBJECT_TYPE_CONTENTS` 동일 emit (한컴
3546/// wire 일치) 하되 boundary 의 wire code 매핑은 분리.
3547#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
3548#[non_exhaustive]
3549pub enum RefContentType {
3550    /// Show page number where the target appears. 한컴: "쪽 번호" (모든 RefType 공통)
3551    #[default]
3552    Page,
3553    /// Show the target's numbering. 의미는 RefType-상대적:
3554    /// - Footnote/Endnote/Caption/Outline: "주 번호" / "표 번호" / "그림 번호" / "개요 번호" (N2=1)
3555    /// - Bookmark: **책갈피 본문 / 번호 텍스트** (N2=1, 한컴 wire 특이성)
3556    Number,
3557    /// Show the target's content. 의미는 RefType-상대적:
3558    /// - Figure/Table/Equation: "캡션 내용" (N2=2)
3559    /// - Outline: "개요 내용" (N2=2)
3560    /// - Bookmark 에서는 *사용 안 함* — 책갈피의 "내용" 의미는
3561    ///   `RefContentType::Number` 가 carry (N2=1).
3562    Contents,
3563    /// Show the bookmark's NAME. 한컴: "책갈피 이름" (Bookmark+N2=2 전용).
3564    /// HWPX wire 의 `OBJECT_TYPE_CONTENTS` 와 동일 문자열로 emit
3565    /// (RefType=TARGET_BOOKMARK 컨텍스트에서 한컴이 의미 결정).
3566    BookmarkName,
3567    /// Show relative position ("위" / "아래"). 한컴: "위/아래" (N2=3).
3568    UpDownPos,
3569    /// Unrecognized ContentType code preserved from wire for forward
3570    /// compatibility.
3571    Unknown(u8),
3572}
3573
3574impl fmt::Display for RefContentType {
3575    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3576        match self {
3577            Self::Page => f.write_str("OBJECT_TYPE_PAGE"),
3578            Self::Number => f.write_str("OBJECT_TYPE_NUMBER"),
3579            // Wave 12p pre-fix: `Contents` 와 `BookmarkName` 모두 한컴
3580            // wire 의 `OBJECT_TYPE_CONTENTS` 로 emit. 의미 구분은
3581            // RefType + Command 의 N2 wire code 에서 (Bookmark N2=2 =
3582            // 책갈피 이름, Figure/Table/Eq/Outline N2=2 = 캡션 내용).
3583            Self::Contents | Self::BookmarkName => f.write_str("OBJECT_TYPE_CONTENTS"),
3584            Self::UpDownPos => f.write_str("OBJECT_TYPE_UPDOWNPOS"),
3585            Self::Unknown(code) => write!(f, "OBJECT_TYPE_UNKNOWN({code})"),
3586        }
3587    }
3588}
3589
3590impl std::str::FromStr for RefContentType {
3591    type Err = FoundationError;
3592
3593    fn from_str(s: &str) -> Result<Self, Self::Err> {
3594        match s {
3595            "OBJECT_TYPE_PAGE" | "Page" | "page" => Ok(Self::Page),
3596            "OBJECT_TYPE_NUMBER" | "Number" | "number" => Ok(Self::Number),
3597            // Wave 12p pre-fix: `OBJECT_TYPE_CONTENTS` 는 caller 의 추가
3598            // 컨텍스트 (RefType + N2 wire code) 없이는 `Contents` 와
3599            // `BookmarkName` 을 구분할 수 없으므로 default 로 `Contents`
3600            // 를 반환. boundary 함수 `decode_hwp5_crossref_content_type`
3601            // 가 Bookmark N2=2 인 경우만 `BookmarkName` 으로 재매핑.
3602            "OBJECT_TYPE_CONTENTS" | "Contents" | "contents" => Ok(Self::Contents),
3603            "BookmarkName" | "bookmark_name" => Ok(Self::BookmarkName),
3604            "OBJECT_TYPE_UPDOWNPOS" | "UpDownPos" | "updownpos" => Ok(Self::UpDownPos),
3605            _ => Err(FoundationError::ParseError {
3606                type_name: "RefContentType".to_string(),
3607                value: s.to_string(),
3608                valid_values: "OBJECT_TYPE_PAGE, OBJECT_TYPE_NUMBER, OBJECT_TYPE_CONTENTS, \
3609                    OBJECT_TYPE_UPDOWNPOS"
3610                    .to_string(),
3611            }),
3612        }
3613    }
3614}
3615
3616impl schemars::JsonSchema for RefContentType {
3617    fn schema_name() -> std::borrow::Cow<'static, str> {
3618        std::borrow::Cow::Borrowed("RefContentType")
3619    }
3620
3621    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3622        gen.subschema_for::<String>()
3623    }
3624}
3625
3626/// Drop cap style for floating shape objects (HWPX `dropcapstyle` attribute).
3627///
3628/// Controls whether a shape (text box, image, table, etc.) is formatted as a
3629/// drop capital that occupies multiple lines at the start of a paragraph.
3630///
3631/// # HWPX Values
3632///
3633/// | Variant      | HWPX string     |
3634/// |--------------|-----------------|
3635/// | `None`       | `"None"`        |
3636/// | `DoubleLine` | `"DoubleLine"`  |
3637/// | `TripleLine` | `"TripleLine"`  |
3638/// | `Margin`     | `"Margin"`      |
3639#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
3640#[non_exhaustive]
3641pub enum DropCapStyle {
3642    /// No drop cap (default).
3643    #[default]
3644    None = 0,
3645    /// Drop cap spanning 2 lines.
3646    DoubleLine = 1,
3647    /// Drop cap spanning 3 lines.
3648    TripleLine = 2,
3649    /// Drop cap positioned in the margin.
3650    Margin = 3,
3651}
3652
3653impl fmt::Display for DropCapStyle {
3654    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3655        match self {
3656            Self::None => f.write_str("None"),
3657            Self::DoubleLine => f.write_str("DoubleLine"),
3658            Self::TripleLine => f.write_str("TripleLine"),
3659            Self::Margin => f.write_str("Margin"),
3660        }
3661    }
3662}
3663
3664impl DropCapStyle {
3665    /// Parses an HWPX `dropcapstyle` attribute value (PascalCase).
3666    ///
3667    /// Unknown values fall back to `None` (default) for forward compatibility.
3668    pub fn from_hwpx_str(s: &str) -> Self {
3669        match s {
3670            "DoubleLine" => Self::DoubleLine,
3671            "TripleLine" => Self::TripleLine,
3672            "Margin" => Self::Margin,
3673            _ => Self::None,
3674        }
3675    }
3676}
3677
3678impl std::str::FromStr for DropCapStyle {
3679    type Err = FoundationError;
3680
3681    fn from_str(s: &str) -> Result<Self, Self::Err> {
3682        match s {
3683            "None" | "NONE" | "none" => Ok(Self::None),
3684            "DoubleLine" | "DOUBLE_LINE" => Ok(Self::DoubleLine),
3685            "TripleLine" | "TRIPLE_LINE" => Ok(Self::TripleLine),
3686            "Margin" | "MARGIN" => Ok(Self::Margin),
3687            _ => Err(FoundationError::ParseError {
3688                type_name: "DropCapStyle".to_string(),
3689                value: s.to_string(),
3690                valid_values: "None, DoubleLine, TripleLine, Margin".to_string(),
3691            }),
3692        }
3693    }
3694}
3695
3696impl schemars::JsonSchema for DropCapStyle {
3697    fn schema_name() -> std::borrow::Cow<'static, str> {
3698        std::borrow::Cow::Borrowed("DropCapStyle")
3699    }
3700
3701    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3702        gen.subschema_for::<String>()
3703    }
3704}
3705
3706impl serde::Serialize for DropCapStyle {
3707    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
3708        serializer.serialize_str(&self.to_string())
3709    }
3710}
3711
3712impl<'de> serde::Deserialize<'de> for DropCapStyle {
3713    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
3714        let s = String::deserialize(deserializer)?;
3715        s.parse().map_err(serde::de::Error::custom)
3716    }
3717}
3718
3719// Compile-time size assertions: all enums are 1 byte
3720const _: () = assert!(std::mem::size_of::<DropCapStyle>() == 1);
3721const _: () = assert!(std::mem::size_of::<Alignment>() == 1);
3722const _: () = assert!(std::mem::size_of::<LineSpacingType>() == 1);
3723const _: () = assert!(std::mem::size_of::<BreakType>() == 1);
3724const _: () = assert!(std::mem::size_of::<Language>() == 1);
3725const _: () = assert!(std::mem::size_of::<UnderlineType>() == 1);
3726const _: () = assert!(std::mem::size_of::<UnderlineShape>() == 1);
3727const _: () = assert!(std::mem::size_of::<StrikeoutShape>() == 1);
3728const _: () = assert!(std::mem::size_of::<OutlineType>() == 1);
3729const _: () = assert!(std::mem::size_of::<ShadowType>() == 1);
3730const _: () = assert!(std::mem::size_of::<EmbossType>() == 1);
3731const _: () = assert!(std::mem::size_of::<EngraveType>() == 1);
3732const _: () = assert!(std::mem::size_of::<VerticalPosition>() == 1);
3733const _: () = assert!(std::mem::size_of::<BorderLineType>() == 1);
3734const _: () = assert!(std::mem::size_of::<FillBrushType>() == 1);
3735const _: () = assert!(std::mem::size_of::<ApplyPageType>() == 1);
3736const _: () = assert!(std::mem::size_of::<NumberFormatType>() == 1);
3737const _: () = assert!(std::mem::size_of::<PageNumberPosition>() == 1);
3738const _: () = assert!(std::mem::size_of::<WordBreakType>() == 1);
3739const _: () = assert!(std::mem::size_of::<EmphasisType>() == 1);
3740const _: () = assert!(std::mem::size_of::<HeadingType>() == 1);
3741const _: () = assert!(std::mem::size_of::<GutterType>() == 1);
3742const _: () = assert!(std::mem::size_of::<ShowMode>() == 1);
3743const _: () = assert!(std::mem::size_of::<RestartType>() == 1);
3744const _: () = assert!(std::mem::size_of::<TextBorderType>() == 1);
3745const _: () = assert!(std::mem::size_of::<Flip>() == 1);
3746const _: () = assert!(std::mem::size_of::<ArcType>() == 1);
3747const _: () = assert!(std::mem::size_of::<ArrowType>() == 1);
3748const _: () = assert!(std::mem::size_of::<ArrowSize>() == 1);
3749const _: () = assert!(std::mem::size_of::<GradientType>() == 1);
3750const _: () = assert!(std::mem::size_of::<PatternType>() == 1);
3751const _: () = assert!(std::mem::size_of::<ImageFillMode>() == 1);
3752const _: () = assert!(std::mem::size_of::<CurveSegmentType>() == 1);
3753const _: () = assert!(std::mem::size_of::<BookmarkType>() == 1);
3754const _: () = assert!(std::mem::size_of::<FieldType>() == 1);
3755// Wave 12m Phase 2: RefType / RefContentType 의 `#[repr(u8)]` 제거 + Unknown(u8)
3756// tuple variant 추가로 size 가 1 byte 에서 2 bytes (discriminant + u8 payload) 로
3757// 증가. trade-off: type-safe forward-compat carry > 1 byte memory.
3758const _: () = assert!(std::mem::size_of::<RefType>() == 2);
3759const _: () = assert!(std::mem::size_of::<RefContentType>() == 2);
3760
3761// ---------------------------------------------------------------------------
3762// TextDirection
3763// ---------------------------------------------------------------------------
3764
3765/// Text writing direction for sections and sub-lists.
3766///
3767/// Controls whether text flows horizontally (가로쓰기) or vertically (세로쓰기).
3768/// Used in `<hp:secPr textDirection="...">` and `<hp:subList textDirection="...">`.
3769///
3770/// # Examples
3771///
3772/// ```
3773/// use hwpforge_foundation::TextDirection;
3774///
3775/// assert_eq!(TextDirection::default(), TextDirection::Horizontal);
3776/// assert_eq!(TextDirection::Horizontal.to_string(), "HORIZONTAL");
3777/// ```
3778#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
3779#[non_exhaustive]
3780pub enum TextDirection {
3781    /// Horizontal writing (가로쓰기) — default.
3782    #[default]
3783    Horizontal,
3784    /// Vertical writing with Latin chars rotated 90° (세로쓰기 영문 눕힘).
3785    Vertical,
3786    /// Vertical writing with Latin chars upright (세로쓰기 영문 세움).
3787    VerticalAll,
3788}
3789
3790impl TextDirection {
3791    /// Parses a HWPX XML attribute string (e.g. `"VERTICAL"`).
3792    ///
3793    /// Unknown values fall back to [`TextDirection::Horizontal`].
3794    pub fn from_hwpx_str(s: &str) -> Self {
3795        match s {
3796            "VERTICAL" => Self::Vertical,
3797            "VERTICALALL" => Self::VerticalAll,
3798            _ => Self::Horizontal,
3799        }
3800    }
3801}
3802
3803impl fmt::Display for TextDirection {
3804    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3805        match self {
3806            Self::Horizontal => f.write_str("HORIZONTAL"),
3807            Self::Vertical => f.write_str("VERTICAL"),
3808            Self::VerticalAll => f.write_str("VERTICALALL"),
3809        }
3810    }
3811}
3812
3813impl schemars::JsonSchema for TextDirection {
3814    fn schema_name() -> std::borrow::Cow<'static, str> {
3815        std::borrow::Cow::Borrowed("TextDirection")
3816    }
3817
3818    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
3819        gen.subschema_for::<String>()
3820    }
3821}
3822
3823const _: () = assert!(std::mem::size_of::<TextDirection>() == 1);
3824
3825#[cfg(test)]
3826mod tests {
3827    use super::*;
3828    use std::str::FromStr;
3829
3830    // ===================================================================
3831    // Alignment (10+ tests)
3832    // ===================================================================
3833
3834    #[test]
3835    fn alignment_default_is_left() {
3836        assert_eq!(Alignment::default(), Alignment::Left);
3837    }
3838
3839    #[test]
3840    fn alignment_display_all_variants() {
3841        assert_eq!(Alignment::Left.to_string(), "Left");
3842        assert_eq!(Alignment::Center.to_string(), "Center");
3843        assert_eq!(Alignment::Right.to_string(), "Right");
3844        assert_eq!(Alignment::Justify.to_string(), "Justify");
3845        assert_eq!(Alignment::Distribute.to_string(), "Distribute");
3846        assert_eq!(Alignment::DistributeFlush.to_string(), "DistributeFlush");
3847    }
3848
3849    #[test]
3850    fn alignment_from_str_pascal_case() {
3851        assert_eq!(Alignment::from_str("Left").unwrap(), Alignment::Left);
3852        assert_eq!(Alignment::from_str("Center").unwrap(), Alignment::Center);
3853        assert_eq!(Alignment::from_str("Right").unwrap(), Alignment::Right);
3854        assert_eq!(Alignment::from_str("Justify").unwrap(), Alignment::Justify);
3855        assert_eq!(Alignment::from_str("Distribute").unwrap(), Alignment::Distribute);
3856        assert_eq!(Alignment::from_str("DistributeFlush").unwrap(), Alignment::DistributeFlush);
3857    }
3858
3859    #[test]
3860    fn alignment_from_str_lower_case() {
3861        assert_eq!(Alignment::from_str("left").unwrap(), Alignment::Left);
3862        assert_eq!(Alignment::from_str("center").unwrap(), Alignment::Center);
3863        assert_eq!(Alignment::from_str("distribute").unwrap(), Alignment::Distribute);
3864        assert_eq!(Alignment::from_str("distributeflush").unwrap(), Alignment::DistributeFlush);
3865        assert_eq!(Alignment::from_str("distribute_flush").unwrap(), Alignment::DistributeFlush);
3866    }
3867
3868    #[test]
3869    fn alignment_from_str_invalid() {
3870        let err = Alignment::from_str("leftt").unwrap_err();
3871        match err {
3872            FoundationError::ParseError { ref type_name, ref value, .. } => {
3873                assert_eq!(type_name, "Alignment");
3874                assert_eq!(value, "leftt");
3875            }
3876            other => panic!("unexpected: {other}"),
3877        }
3878    }
3879
3880    #[test]
3881    fn alignment_try_from_u8() {
3882        assert_eq!(Alignment::try_from(0u8).unwrap(), Alignment::Left);
3883        assert_eq!(Alignment::try_from(1u8).unwrap(), Alignment::Center);
3884        assert_eq!(Alignment::try_from(2u8).unwrap(), Alignment::Right);
3885        assert_eq!(Alignment::try_from(3u8).unwrap(), Alignment::Justify);
3886        assert_eq!(Alignment::try_from(4u8).unwrap(), Alignment::Distribute);
3887        assert_eq!(Alignment::try_from(5u8).unwrap(), Alignment::DistributeFlush);
3888        assert!(Alignment::try_from(6u8).is_err());
3889        assert!(Alignment::try_from(255u8).is_err());
3890    }
3891
3892    #[test]
3893    fn alignment_repr_values() {
3894        assert_eq!(Alignment::Left as u8, 0);
3895        assert_eq!(Alignment::Center as u8, 1);
3896        assert_eq!(Alignment::Right as u8, 2);
3897        assert_eq!(Alignment::Justify as u8, 3);
3898        assert_eq!(Alignment::Distribute as u8, 4);
3899        assert_eq!(Alignment::DistributeFlush as u8, 5);
3900    }
3901
3902    #[test]
3903    fn alignment_serde_roundtrip() {
3904        for variant in &[
3905            Alignment::Left,
3906            Alignment::Center,
3907            Alignment::Right,
3908            Alignment::Justify,
3909            Alignment::Distribute,
3910            Alignment::DistributeFlush,
3911        ] {
3912            let json = serde_json::to_string(variant).unwrap();
3913            let back: Alignment = serde_json::from_str(&json).unwrap();
3914            assert_eq!(&back, variant);
3915        }
3916    }
3917
3918    #[test]
3919    fn alignment_str_roundtrip() {
3920        for variant in &[
3921            Alignment::Left,
3922            Alignment::Center,
3923            Alignment::Right,
3924            Alignment::Justify,
3925            Alignment::Distribute,
3926            Alignment::DistributeFlush,
3927        ] {
3928            let s = variant.to_string();
3929            let back = Alignment::from_str(&s).unwrap();
3930            assert_eq!(&back, variant);
3931        }
3932    }
3933
3934    #[test]
3935    fn alignment_copy_and_hash() {
3936        use std::collections::HashSet;
3937        let a = Alignment::Left;
3938        let b = a; // Copy
3939        assert_eq!(a, b);
3940
3941        let mut set = HashSet::new();
3942        set.insert(Alignment::Left);
3943        set.insert(Alignment::Right);
3944        assert_eq!(set.len(), 2);
3945    }
3946
3947    // ===================================================================
3948    // LineSpacingType
3949    // ===================================================================
3950
3951    #[test]
3952    fn line_spacing_default_is_percentage() {
3953        assert_eq!(LineSpacingType::default(), LineSpacingType::Percentage);
3954    }
3955
3956    #[test]
3957    fn line_spacing_display() {
3958        assert_eq!(LineSpacingType::Percentage.to_string(), "Percentage");
3959        assert_eq!(LineSpacingType::Fixed.to_string(), "Fixed");
3960        assert_eq!(LineSpacingType::BetweenLines.to_string(), "BetweenLines");
3961        assert_eq!(LineSpacingType::AtLeast.to_string(), "AtLeast");
3962    }
3963
3964    #[test]
3965    fn line_spacing_from_str() {
3966        assert_eq!(LineSpacingType::from_str("Percentage").unwrap(), LineSpacingType::Percentage);
3967        assert_eq!(LineSpacingType::from_str("Fixed").unwrap(), LineSpacingType::Fixed);
3968        assert_eq!(
3969            LineSpacingType::from_str("BetweenLines").unwrap(),
3970            LineSpacingType::BetweenLines
3971        );
3972        assert_eq!(LineSpacingType::from_str("AtLeast").unwrap(), LineSpacingType::AtLeast);
3973        assert_eq!(LineSpacingType::from_str("at_least").unwrap(), LineSpacingType::AtLeast);
3974        assert!(LineSpacingType::from_str("invalid").is_err());
3975    }
3976
3977    #[test]
3978    fn line_spacing_try_from_u8() {
3979        assert_eq!(LineSpacingType::try_from(0u8).unwrap(), LineSpacingType::Percentage);
3980        assert_eq!(LineSpacingType::try_from(1u8).unwrap(), LineSpacingType::Fixed);
3981        assert_eq!(LineSpacingType::try_from(2u8).unwrap(), LineSpacingType::BetweenLines);
3982        assert_eq!(LineSpacingType::try_from(3u8).unwrap(), LineSpacingType::AtLeast);
3983        assert!(LineSpacingType::try_from(4u8).is_err());
3984    }
3985
3986    #[test]
3987    fn line_spacing_str_roundtrip() {
3988        for v in &[
3989            LineSpacingType::Percentage,
3990            LineSpacingType::Fixed,
3991            LineSpacingType::BetweenLines,
3992            LineSpacingType::AtLeast,
3993        ] {
3994            let s = v.to_string();
3995            let back = LineSpacingType::from_str(&s).unwrap();
3996            assert_eq!(&back, v);
3997        }
3998    }
3999
4000    // ===================================================================
4001    // BreakType
4002    // ===================================================================
4003
4004    #[test]
4005    fn break_type_default_is_none() {
4006        assert_eq!(BreakType::default(), BreakType::None);
4007    }
4008
4009    #[test]
4010    fn break_type_display() {
4011        assert_eq!(BreakType::None.to_string(), "None");
4012        assert_eq!(BreakType::Column.to_string(), "Column");
4013        assert_eq!(BreakType::Page.to_string(), "Page");
4014    }
4015
4016    #[test]
4017    fn break_type_from_str() {
4018        assert_eq!(BreakType::from_str("None").unwrap(), BreakType::None);
4019        assert_eq!(BreakType::from_str("Column").unwrap(), BreakType::Column);
4020        assert_eq!(BreakType::from_str("Page").unwrap(), BreakType::Page);
4021        assert!(BreakType::from_str("section").is_err());
4022    }
4023
4024    #[test]
4025    fn break_type_try_from_u8() {
4026        assert_eq!(BreakType::try_from(0u8).unwrap(), BreakType::None);
4027        assert_eq!(BreakType::try_from(1u8).unwrap(), BreakType::Column);
4028        assert_eq!(BreakType::try_from(2u8).unwrap(), BreakType::Page);
4029        assert!(BreakType::try_from(3u8).is_err());
4030    }
4031
4032    #[test]
4033    fn break_type_str_roundtrip() {
4034        for v in &[BreakType::None, BreakType::Column, BreakType::Page] {
4035            let s = v.to_string();
4036            let back = BreakType::from_str(&s).unwrap();
4037            assert_eq!(&back, v);
4038        }
4039    }
4040
4041    // ===================================================================
4042    // Language
4043    // ===================================================================
4044
4045    #[test]
4046    fn language_count_is_7() {
4047        assert_eq!(Language::COUNT, 7);
4048        assert_eq!(Language::ALL.len(), 7);
4049    }
4050
4051    #[test]
4052    fn language_default_is_korean() {
4053        assert_eq!(Language::default(), Language::Korean);
4054    }
4055
4056    #[test]
4057    fn language_discriminants() {
4058        assert_eq!(Language::Korean as u8, 0);
4059        assert_eq!(Language::English as u8, 1);
4060        assert_eq!(Language::Hanja as u8, 2);
4061        assert_eq!(Language::Japanese as u8, 3);
4062        assert_eq!(Language::Other as u8, 4);
4063        assert_eq!(Language::Symbol as u8, 5);
4064        assert_eq!(Language::User as u8, 6);
4065    }
4066
4067    #[test]
4068    fn language_display() {
4069        assert_eq!(Language::Korean.to_string(), "Korean");
4070        assert_eq!(Language::English.to_string(), "English");
4071        assert_eq!(Language::Japanese.to_string(), "Japanese");
4072    }
4073
4074    #[test]
4075    fn language_from_str() {
4076        for lang in &Language::ALL {
4077            let s = lang.to_string();
4078            let back = Language::from_str(&s).unwrap();
4079            assert_eq!(&back, lang);
4080        }
4081        assert!(Language::from_str("invalid").is_err());
4082    }
4083
4084    #[test]
4085    fn language_try_from_u8() {
4086        for (i, expected) in Language::ALL.iter().enumerate() {
4087            let parsed = Language::try_from(i as u8).unwrap();
4088            assert_eq!(&parsed, expected);
4089        }
4090        assert!(Language::try_from(7u8).is_err());
4091        assert!(Language::try_from(255u8).is_err());
4092    }
4093
4094    #[test]
4095    fn language_all_used_as_index() {
4096        // Common pattern: fonts[lang as usize]
4097        let fonts: [&str; Language::COUNT] =
4098            ["Batang", "Arial", "SimSun", "MS Mincho", "Arial", "Symbol", "Arial"];
4099        for lang in &Language::ALL {
4100            let _ = fonts[*lang as usize];
4101        }
4102    }
4103
4104    #[test]
4105    fn language_serde_roundtrip() {
4106        for lang in &Language::ALL {
4107            let json = serde_json::to_string(lang).unwrap();
4108            let back: Language = serde_json::from_str(&json).unwrap();
4109            assert_eq!(&back, lang);
4110        }
4111    }
4112
4113    // ===================================================================
4114    // UnderlineType
4115    // ===================================================================
4116
4117    #[test]
4118    fn underline_type_default_is_none() {
4119        assert_eq!(UnderlineType::default(), UnderlineType::None);
4120    }
4121
4122    #[test]
4123    fn underline_type_display() {
4124        assert_eq!(UnderlineType::None.to_string(), "None");
4125        assert_eq!(UnderlineType::Bottom.to_string(), "Bottom");
4126        assert_eq!(UnderlineType::Center.to_string(), "Center");
4127        assert_eq!(UnderlineType::Top.to_string(), "Top");
4128    }
4129
4130    #[test]
4131    fn underline_type_from_str() {
4132        assert_eq!(UnderlineType::from_str("None").unwrap(), UnderlineType::None);
4133        assert_eq!(UnderlineType::from_str("Bottom").unwrap(), UnderlineType::Bottom);
4134        assert_eq!(UnderlineType::from_str("center").unwrap(), UnderlineType::Center);
4135        assert!(UnderlineType::from_str("invalid").is_err());
4136    }
4137
4138    #[test]
4139    fn underline_type_try_from_u8() {
4140        assert_eq!(UnderlineType::try_from(0u8).unwrap(), UnderlineType::None);
4141        assert_eq!(UnderlineType::try_from(1u8).unwrap(), UnderlineType::Bottom);
4142        assert_eq!(UnderlineType::try_from(2u8).unwrap(), UnderlineType::Center);
4143        assert_eq!(UnderlineType::try_from(3u8).unwrap(), UnderlineType::Top);
4144        assert!(UnderlineType::try_from(4u8).is_err());
4145    }
4146
4147    #[test]
4148    fn underline_type_str_roundtrip() {
4149        for v in
4150            &[UnderlineType::None, UnderlineType::Bottom, UnderlineType::Center, UnderlineType::Top]
4151        {
4152            let s = v.to_string();
4153            let back = UnderlineType::from_str(&s).unwrap();
4154            assert_eq!(&back, v);
4155        }
4156    }
4157
4158    // ===================================================================
4159    // UnderlineShape
4160    // ===================================================================
4161
4162    #[test]
4163    fn underline_shape_default_is_solid() {
4164        assert_eq!(UnderlineShape::default(), UnderlineShape::Solid);
4165    }
4166
4167    #[test]
4168    fn underline_shape_display_screaming_snake_case() {
4169        assert_eq!(UnderlineShape::Solid.to_string(), "SOLID");
4170        assert_eq!(UnderlineShape::Dash.to_string(), "DASH");
4171        assert_eq!(UnderlineShape::Dot.to_string(), "DOT");
4172        assert_eq!(UnderlineShape::DashDot.to_string(), "DASH_DOT");
4173        assert_eq!(UnderlineShape::DashDotDot.to_string(), "DASH_DOT_DOT");
4174        assert_eq!(UnderlineShape::LongDash.to_string(), "LONG_DASH");
4175        assert_eq!(UnderlineShape::Circle.to_string(), "CIRCLE");
4176        assert_eq!(UnderlineShape::DoubleSlim.to_string(), "DOUBLE_SLIM");
4177        assert_eq!(UnderlineShape::SlimThick.to_string(), "SLIM_THICK");
4178        assert_eq!(UnderlineShape::ThickSlim.to_string(), "THICK_SLIM");
4179        assert_eq!(UnderlineShape::ThickSlimThick.to_string(), "THICK_SLIM_THICK");
4180        assert_eq!(UnderlineShape::Wave.to_string(), "WAVE");
4181    }
4182
4183    #[test]
4184    fn underline_shape_from_str_variants() {
4185        assert_eq!(UnderlineShape::from_str("SOLID").unwrap(), UnderlineShape::Solid);
4186        assert_eq!(UnderlineShape::from_str("dash").unwrap(), UnderlineShape::Dash);
4187        assert_eq!(UnderlineShape::from_str("DOUBLE_SLIM").unwrap(), UnderlineShape::DoubleSlim);
4188        assert_eq!(UnderlineShape::from_str("WAVE").unwrap(), UnderlineShape::Wave);
4189        assert!(UnderlineShape::from_str("invalid").is_err());
4190    }
4191
4192    #[test]
4193    fn underline_shape_try_from_u8() {
4194        assert_eq!(UnderlineShape::try_from(0u8).unwrap(), UnderlineShape::Solid);
4195        assert_eq!(UnderlineShape::try_from(1u8).unwrap(), UnderlineShape::Dash);
4196        assert_eq!(UnderlineShape::try_from(7u8).unwrap(), UnderlineShape::DoubleSlim);
4197        assert_eq!(UnderlineShape::try_from(11u8).unwrap(), UnderlineShape::Wave);
4198        assert!(UnderlineShape::try_from(12u8).is_err());
4199    }
4200
4201    #[test]
4202    fn underline_shape_str_roundtrip() {
4203        for v in &[
4204            UnderlineShape::Solid,
4205            UnderlineShape::Dash,
4206            UnderlineShape::Dot,
4207            UnderlineShape::DashDot,
4208            UnderlineShape::DashDotDot,
4209            UnderlineShape::LongDash,
4210            UnderlineShape::Circle,
4211            UnderlineShape::DoubleSlim,
4212            UnderlineShape::SlimThick,
4213            UnderlineShape::ThickSlim,
4214            UnderlineShape::ThickSlimThick,
4215            UnderlineShape::Wave,
4216        ] {
4217            let s = v.to_string();
4218            let back = UnderlineShape::from_str(&s).unwrap();
4219            assert_eq!(&back, v);
4220        }
4221    }
4222
4223    // ===================================================================
4224    // StrikeoutShape
4225    // ===================================================================
4226
4227    #[test]
4228    fn strikeout_shape_default_is_none() {
4229        assert_eq!(StrikeoutShape::default(), StrikeoutShape::None);
4230    }
4231
4232    #[test]
4233    fn strikeout_shape_display() {
4234        assert_eq!(StrikeoutShape::None.to_string(), "NONE");
4235        assert_eq!(StrikeoutShape::Solid.to_string(), "SOLID");
4236        assert_eq!(StrikeoutShape::Dash.to_string(), "DASH");
4237        assert_eq!(StrikeoutShape::Dot.to_string(), "DOT");
4238        assert_eq!(StrikeoutShape::DashDot.to_string(), "DASH_DOT");
4239        assert_eq!(StrikeoutShape::DashDotDot.to_string(), "DASH_DOT_DOT");
4240        assert_eq!(StrikeoutShape::LongDash.to_string(), "LONG_DASH");
4241        assert_eq!(StrikeoutShape::Circle.to_string(), "CIRCLE");
4242        assert_eq!(StrikeoutShape::DoubleSlim.to_string(), "DOUBLE_SLIM");
4243        assert_eq!(StrikeoutShape::SlimThick.to_string(), "SLIM_THICK");
4244        assert_eq!(StrikeoutShape::ThickSlim.to_string(), "THICK_SLIM");
4245        assert_eq!(StrikeoutShape::ThickSlimThick.to_string(), "THICK_SLIM_THICK");
4246        assert_eq!(StrikeoutShape::Wave.to_string(), "WAVE");
4247    }
4248
4249    #[test]
4250    fn strikeout_shape_from_str() {
4251        assert_eq!(StrikeoutShape::from_str("NONE").unwrap(), StrikeoutShape::None);
4252        assert_eq!(StrikeoutShape::from_str("SOLID").unwrap(), StrikeoutShape::Solid);
4253        // Backward-compatible alias for the pre-Wave-1c name.
4254        assert_eq!(StrikeoutShape::from_str("Continuous").unwrap(), StrikeoutShape::Solid);
4255        assert_eq!(StrikeoutShape::from_str("dash_dot").unwrap(), StrikeoutShape::DashDot);
4256        assert_eq!(StrikeoutShape::from_str("DOUBLE_SLIM").unwrap(), StrikeoutShape::DoubleSlim);
4257        assert_eq!(StrikeoutShape::from_str("WAVE").unwrap(), StrikeoutShape::Wave);
4258        assert!(StrikeoutShape::from_str("invalid").is_err());
4259    }
4260
4261    #[test]
4262    fn strikeout_shape_try_from_u8() {
4263        assert_eq!(StrikeoutShape::try_from(0u8).unwrap(), StrikeoutShape::None);
4264        assert_eq!(StrikeoutShape::try_from(1u8).unwrap(), StrikeoutShape::Solid);
4265        assert_eq!(StrikeoutShape::try_from(5u8).unwrap(), StrikeoutShape::DashDotDot);
4266        assert_eq!(StrikeoutShape::try_from(8u8).unwrap(), StrikeoutShape::DoubleSlim);
4267        assert_eq!(StrikeoutShape::try_from(12u8).unwrap(), StrikeoutShape::Wave);
4268        assert!(StrikeoutShape::try_from(13u8).is_err());
4269    }
4270
4271    #[test]
4272    fn strikeout_shape_str_roundtrip() {
4273        for v in &[
4274            StrikeoutShape::None,
4275            StrikeoutShape::Solid,
4276            StrikeoutShape::Dash,
4277            StrikeoutShape::Dot,
4278            StrikeoutShape::DashDot,
4279            StrikeoutShape::DashDotDot,
4280            StrikeoutShape::LongDash,
4281            StrikeoutShape::Circle,
4282            StrikeoutShape::DoubleSlim,
4283            StrikeoutShape::SlimThick,
4284            StrikeoutShape::ThickSlim,
4285            StrikeoutShape::ThickSlimThick,
4286            StrikeoutShape::Wave,
4287        ] {
4288            let s = v.to_string();
4289            let back = StrikeoutShape::from_str(&s).unwrap();
4290            assert_eq!(&back, v);
4291        }
4292    }
4293
4294    // ===================================================================
4295    // OutlineType
4296    // ===================================================================
4297
4298    #[test]
4299    fn outline_type_default_is_none() {
4300        assert_eq!(OutlineType::default(), OutlineType::None);
4301    }
4302
4303    #[test]
4304    fn outline_type_display() {
4305        assert_eq!(OutlineType::None.to_string(), "None");
4306        assert_eq!(OutlineType::Solid.to_string(), "Solid");
4307    }
4308
4309    #[test]
4310    fn outline_type_from_str() {
4311        assert_eq!(OutlineType::from_str("None").unwrap(), OutlineType::None);
4312        assert_eq!(OutlineType::from_str("solid").unwrap(), OutlineType::Solid);
4313        assert!(OutlineType::from_str("dashed").is_err());
4314    }
4315
4316    #[test]
4317    fn outline_type_try_from_u8() {
4318        assert_eq!(OutlineType::try_from(0u8).unwrap(), OutlineType::None);
4319        assert_eq!(OutlineType::try_from(1u8).unwrap(), OutlineType::Solid);
4320        assert!(OutlineType::try_from(2u8).is_err());
4321    }
4322
4323    // ===================================================================
4324    // ShadowType
4325    // ===================================================================
4326
4327    #[test]
4328    fn shadow_type_default_is_none() {
4329        assert_eq!(ShadowType::default(), ShadowType::None);
4330    }
4331
4332    #[test]
4333    fn shadow_type_display() {
4334        assert_eq!(ShadowType::None.to_string(), "None");
4335        assert_eq!(ShadowType::Drop.to_string(), "Drop");
4336    }
4337
4338    #[test]
4339    fn shadow_type_from_str() {
4340        assert_eq!(ShadowType::from_str("None").unwrap(), ShadowType::None);
4341        assert_eq!(ShadowType::from_str("drop").unwrap(), ShadowType::Drop);
4342        assert!(ShadowType::from_str("shadow").is_err());
4343    }
4344
4345    #[test]
4346    fn shadow_type_try_from_u8() {
4347        assert_eq!(ShadowType::try_from(0u8).unwrap(), ShadowType::None);
4348        assert_eq!(ShadowType::try_from(1u8).unwrap(), ShadowType::Drop);
4349        assert!(ShadowType::try_from(2u8).is_err());
4350    }
4351
4352    // ===================================================================
4353    // EmbossType
4354    // ===================================================================
4355
4356    #[test]
4357    fn emboss_type_default_is_none() {
4358        assert_eq!(EmbossType::default(), EmbossType::None);
4359    }
4360
4361    #[test]
4362    fn emboss_type_display() {
4363        assert_eq!(EmbossType::None.to_string(), "None");
4364        assert_eq!(EmbossType::Emboss.to_string(), "Emboss");
4365    }
4366
4367    #[test]
4368    fn emboss_type_from_str() {
4369        assert_eq!(EmbossType::from_str("None").unwrap(), EmbossType::None);
4370        assert_eq!(EmbossType::from_str("emboss").unwrap(), EmbossType::Emboss);
4371        assert!(EmbossType::from_str("raised").is_err());
4372    }
4373
4374    #[test]
4375    fn emboss_type_try_from_u8() {
4376        assert_eq!(EmbossType::try_from(0u8).unwrap(), EmbossType::None);
4377        assert_eq!(EmbossType::try_from(1u8).unwrap(), EmbossType::Emboss);
4378        assert!(EmbossType::try_from(2u8).is_err());
4379    }
4380
4381    // ===================================================================
4382    // EngraveType
4383    // ===================================================================
4384
4385    #[test]
4386    fn engrave_type_default_is_none() {
4387        assert_eq!(EngraveType::default(), EngraveType::None);
4388    }
4389
4390    #[test]
4391    fn engrave_type_display() {
4392        assert_eq!(EngraveType::None.to_string(), "None");
4393        assert_eq!(EngraveType::Engrave.to_string(), "Engrave");
4394    }
4395
4396    #[test]
4397    fn engrave_type_from_str() {
4398        assert_eq!(EngraveType::from_str("None").unwrap(), EngraveType::None);
4399        assert_eq!(EngraveType::from_str("engrave").unwrap(), EngraveType::Engrave);
4400        assert!(EngraveType::from_str("sunken").is_err());
4401    }
4402
4403    #[test]
4404    fn engrave_type_try_from_u8() {
4405        assert_eq!(EngraveType::try_from(0u8).unwrap(), EngraveType::None);
4406        assert_eq!(EngraveType::try_from(1u8).unwrap(), EngraveType::Engrave);
4407        assert!(EngraveType::try_from(2u8).is_err());
4408    }
4409
4410    // ===================================================================
4411    // VerticalPosition
4412    // ===================================================================
4413
4414    #[test]
4415    fn vertical_position_default_is_normal() {
4416        assert_eq!(VerticalPosition::default(), VerticalPosition::Normal);
4417    }
4418
4419    #[test]
4420    fn vertical_position_display() {
4421        assert_eq!(VerticalPosition::Normal.to_string(), "Normal");
4422        assert_eq!(VerticalPosition::Superscript.to_string(), "Superscript");
4423        assert_eq!(VerticalPosition::Subscript.to_string(), "Subscript");
4424    }
4425
4426    #[test]
4427    fn vertical_position_from_str() {
4428        assert_eq!(VerticalPosition::from_str("Normal").unwrap(), VerticalPosition::Normal);
4429        assert_eq!(
4430            VerticalPosition::from_str("superscript").unwrap(),
4431            VerticalPosition::Superscript
4432        );
4433        assert_eq!(VerticalPosition::from_str("sub").unwrap(), VerticalPosition::Subscript);
4434        assert!(VerticalPosition::from_str("middle").is_err());
4435    }
4436
4437    #[test]
4438    fn vertical_position_try_from_u8() {
4439        assert_eq!(VerticalPosition::try_from(0u8).unwrap(), VerticalPosition::Normal);
4440        assert_eq!(VerticalPosition::try_from(1u8).unwrap(), VerticalPosition::Superscript);
4441        assert_eq!(VerticalPosition::try_from(2u8).unwrap(), VerticalPosition::Subscript);
4442        assert!(VerticalPosition::try_from(3u8).is_err());
4443    }
4444
4445    #[test]
4446    fn vertical_position_str_roundtrip() {
4447        for v in
4448            &[VerticalPosition::Normal, VerticalPosition::Superscript, VerticalPosition::Subscript]
4449        {
4450            let s = v.to_string();
4451            let back = VerticalPosition::from_str(&s).unwrap();
4452            assert_eq!(&back, v);
4453        }
4454    }
4455
4456    // ===================================================================
4457    // BorderLineType
4458    // ===================================================================
4459
4460    #[test]
4461    fn border_line_type_default_is_none() {
4462        assert_eq!(BorderLineType::default(), BorderLineType::None);
4463    }
4464
4465    #[test]
4466    fn border_line_type_display() {
4467        assert_eq!(BorderLineType::None.to_string(), "None");
4468        assert_eq!(BorderLineType::Solid.to_string(), "Solid");
4469        assert_eq!(BorderLineType::DashDot.to_string(), "DashDot");
4470        assert_eq!(BorderLineType::ThickBetweenSlim.to_string(), "ThickBetweenSlim");
4471    }
4472
4473    #[test]
4474    fn border_line_type_from_str() {
4475        assert_eq!(BorderLineType::from_str("None").unwrap(), BorderLineType::None);
4476        assert_eq!(BorderLineType::from_str("solid").unwrap(), BorderLineType::Solid);
4477        assert_eq!(BorderLineType::from_str("dash_dot").unwrap(), BorderLineType::DashDot);
4478        assert_eq!(BorderLineType::from_str("double").unwrap(), BorderLineType::Double);
4479        assert!(BorderLineType::from_str("wavy").is_err());
4480    }
4481
4482    #[test]
4483    fn border_line_type_try_from_u8() {
4484        assert_eq!(BorderLineType::try_from(0u8).unwrap(), BorderLineType::None);
4485        assert_eq!(BorderLineType::try_from(1u8).unwrap(), BorderLineType::Solid);
4486        assert_eq!(BorderLineType::try_from(10u8).unwrap(), BorderLineType::ThickBetweenSlim);
4487        assert!(BorderLineType::try_from(11u8).is_err());
4488    }
4489
4490    #[test]
4491    fn border_line_type_str_roundtrip() {
4492        for v in &[
4493            BorderLineType::None,
4494            BorderLineType::Solid,
4495            BorderLineType::Dash,
4496            BorderLineType::Dot,
4497            BorderLineType::DashDot,
4498            BorderLineType::DashDotDot,
4499            BorderLineType::LongDash,
4500            BorderLineType::TripleDot,
4501            BorderLineType::Double,
4502            BorderLineType::DoubleSlim,
4503            BorderLineType::ThickBetweenSlim,
4504        ] {
4505            let s = v.to_string();
4506            let back = BorderLineType::from_str(&s).unwrap();
4507            assert_eq!(&back, v);
4508        }
4509    }
4510
4511    // ===================================================================
4512    // FillBrushType
4513    // ===================================================================
4514
4515    #[test]
4516    fn fill_brush_type_default_is_none() {
4517        assert_eq!(FillBrushType::default(), FillBrushType::None);
4518    }
4519
4520    #[test]
4521    fn fill_brush_type_display() {
4522        assert_eq!(FillBrushType::None.to_string(), "None");
4523        assert_eq!(FillBrushType::Solid.to_string(), "Solid");
4524        assert_eq!(FillBrushType::Gradient.to_string(), "Gradient");
4525        assert_eq!(FillBrushType::Pattern.to_string(), "Pattern");
4526    }
4527
4528    #[test]
4529    fn fill_brush_type_from_str() {
4530        assert_eq!(FillBrushType::from_str("None").unwrap(), FillBrushType::None);
4531        assert_eq!(FillBrushType::from_str("solid").unwrap(), FillBrushType::Solid);
4532        assert_eq!(FillBrushType::from_str("gradient").unwrap(), FillBrushType::Gradient);
4533        assert!(FillBrushType::from_str("texture").is_err());
4534    }
4535
4536    #[test]
4537    fn fill_brush_type_try_from_u8() {
4538        assert_eq!(FillBrushType::try_from(0u8).unwrap(), FillBrushType::None);
4539        assert_eq!(FillBrushType::try_from(1u8).unwrap(), FillBrushType::Solid);
4540        assert_eq!(FillBrushType::try_from(2u8).unwrap(), FillBrushType::Gradient);
4541        assert_eq!(FillBrushType::try_from(3u8).unwrap(), FillBrushType::Pattern);
4542        assert!(FillBrushType::try_from(4u8).is_err());
4543    }
4544
4545    #[test]
4546    fn fill_brush_type_str_roundtrip() {
4547        for v in &[
4548            FillBrushType::None,
4549            FillBrushType::Solid,
4550            FillBrushType::Gradient,
4551            FillBrushType::Pattern,
4552        ] {
4553            let s = v.to_string();
4554            let back = FillBrushType::from_str(&s).unwrap();
4555            assert_eq!(&back, v);
4556        }
4557    }
4558
4559    // ===================================================================
4560    // Cross-enum size assertions (compile-time already, but test at runtime too)
4561    // ===================================================================
4562
4563    #[test]
4564    fn all_enums_are_one_byte() {
4565        assert_eq!(std::mem::size_of::<Alignment>(), 1);
4566        assert_eq!(std::mem::size_of::<LineSpacingType>(), 1);
4567        assert_eq!(std::mem::size_of::<BreakType>(), 1);
4568        assert_eq!(std::mem::size_of::<Language>(), 1);
4569        assert_eq!(std::mem::size_of::<UnderlineType>(), 1);
4570        assert_eq!(std::mem::size_of::<StrikeoutShape>(), 1);
4571        assert_eq!(std::mem::size_of::<OutlineType>(), 1);
4572        assert_eq!(std::mem::size_of::<ShadowType>(), 1);
4573        assert_eq!(std::mem::size_of::<EmbossType>(), 1);
4574        assert_eq!(std::mem::size_of::<EngraveType>(), 1);
4575        assert_eq!(std::mem::size_of::<VerticalPosition>(), 1);
4576        assert_eq!(std::mem::size_of::<BorderLineType>(), 1);
4577        assert_eq!(std::mem::size_of::<FillBrushType>(), 1);
4578        assert_eq!(std::mem::size_of::<ApplyPageType>(), 1);
4579        assert_eq!(std::mem::size_of::<NumberFormatType>(), 1);
4580        assert_eq!(std::mem::size_of::<PageNumberPosition>(), 1);
4581    }
4582
4583    // ===================================================================
4584    // ApplyPageType
4585    // ===================================================================
4586
4587    #[test]
4588    fn apply_page_type_default_is_both() {
4589        assert_eq!(ApplyPageType::default(), ApplyPageType::Both);
4590    }
4591
4592    #[test]
4593    fn apply_page_type_display() {
4594        assert_eq!(ApplyPageType::Both.to_string(), "Both");
4595        assert_eq!(ApplyPageType::Even.to_string(), "Even");
4596        assert_eq!(ApplyPageType::Odd.to_string(), "Odd");
4597    }
4598
4599    #[test]
4600    fn apply_page_type_from_str() {
4601        assert_eq!(ApplyPageType::from_str("Both").unwrap(), ApplyPageType::Both);
4602        assert_eq!(ApplyPageType::from_str("BOTH").unwrap(), ApplyPageType::Both);
4603        assert_eq!(ApplyPageType::from_str("even").unwrap(), ApplyPageType::Even);
4604        assert_eq!(ApplyPageType::from_str("ODD").unwrap(), ApplyPageType::Odd);
4605        assert!(ApplyPageType::from_str("invalid").is_err());
4606    }
4607
4608    #[test]
4609    fn apply_page_type_try_from_u8() {
4610        assert_eq!(ApplyPageType::try_from(0u8).unwrap(), ApplyPageType::Both);
4611        assert_eq!(ApplyPageType::try_from(1u8).unwrap(), ApplyPageType::Even);
4612        assert_eq!(ApplyPageType::try_from(2u8).unwrap(), ApplyPageType::Odd);
4613        assert!(ApplyPageType::try_from(3u8).is_err());
4614    }
4615
4616    #[test]
4617    fn apply_page_type_str_roundtrip() {
4618        for v in &[ApplyPageType::Both, ApplyPageType::Even, ApplyPageType::Odd] {
4619            let s = v.to_string();
4620            let back = ApplyPageType::from_str(&s).unwrap();
4621            assert_eq!(&back, v);
4622        }
4623    }
4624
4625    // ===================================================================
4626    // NumberFormatType
4627    // ===================================================================
4628
4629    #[test]
4630    fn number_format_type_default_is_digit() {
4631        assert_eq!(NumberFormatType::default(), NumberFormatType::Digit);
4632    }
4633
4634    #[test]
4635    fn number_format_type_display() {
4636        assert_eq!(NumberFormatType::Digit.to_string(), "Digit");
4637        assert_eq!(NumberFormatType::CircledDigit.to_string(), "CircledDigit");
4638        assert_eq!(NumberFormatType::RomanCapital.to_string(), "RomanCapital");
4639        assert_eq!(NumberFormatType::HanjaDigit.to_string(), "HanjaDigit");
4640    }
4641
4642    #[test]
4643    fn number_format_type_from_str() {
4644        assert_eq!(NumberFormatType::from_str("Digit").unwrap(), NumberFormatType::Digit);
4645        assert_eq!(NumberFormatType::from_str("DIGIT").unwrap(), NumberFormatType::Digit);
4646        assert_eq!(
4647            NumberFormatType::from_str("CircledDigit").unwrap(),
4648            NumberFormatType::CircledDigit
4649        );
4650        assert_eq!(
4651            NumberFormatType::from_str("ROMAN_CAPITAL").unwrap(),
4652            NumberFormatType::RomanCapital
4653        );
4654        assert!(NumberFormatType::from_str("invalid").is_err());
4655    }
4656
4657    #[test]
4658    fn number_format_type_try_from_u8() {
4659        assert_eq!(NumberFormatType::try_from(0u8).unwrap(), NumberFormatType::Digit);
4660        assert_eq!(NumberFormatType::try_from(1u8).unwrap(), NumberFormatType::CircledDigit);
4661        assert_eq!(NumberFormatType::try_from(8u8).unwrap(), NumberFormatType::HanjaDigit);
4662        assert_eq!(
4663            NumberFormatType::try_from(9u8).unwrap(),
4664            NumberFormatType::CircledHangulSyllable
4665        );
4666        assert_eq!(NumberFormatType::try_from(10u8).unwrap(), NumberFormatType::CircledLatinSmall);
4667        assert!(NumberFormatType::try_from(11u8).is_err());
4668    }
4669
4670    #[test]
4671    fn number_format_type_circled_hangul_syllable() {
4672        assert_eq!(NumberFormatType::CircledHangulSyllable.to_string(), "CircledHangulSyllable");
4673        assert_eq!(
4674            NumberFormatType::from_str("CircledHangulSyllable").unwrap(),
4675            NumberFormatType::CircledHangulSyllable
4676        );
4677        assert_eq!(
4678            NumberFormatType::from_str("CIRCLED_HANGUL_SYLLABLE").unwrap(),
4679            NumberFormatType::CircledHangulSyllable
4680        );
4681    }
4682
4683    #[test]
4684    fn number_format_type_str_roundtrip() {
4685        for v in &[
4686            NumberFormatType::Digit,
4687            NumberFormatType::CircledDigit,
4688            NumberFormatType::RomanCapital,
4689            NumberFormatType::RomanSmall,
4690            NumberFormatType::LatinCapital,
4691            NumberFormatType::LatinSmall,
4692            NumberFormatType::HangulSyllable,
4693            NumberFormatType::HangulJamo,
4694            NumberFormatType::HanjaDigit,
4695            NumberFormatType::CircledHangulSyllable,
4696            NumberFormatType::CircledLatinSmall,
4697        ] {
4698            let s = v.to_string();
4699            let back = NumberFormatType::from_str(&s).unwrap();
4700            assert_eq!(&back, v);
4701        }
4702    }
4703
4704    // ===================================================================
4705    // PageNumberPosition
4706    // ===================================================================
4707
4708    #[test]
4709    fn page_number_position_default_is_top_center() {
4710        assert_eq!(PageNumberPosition::default(), PageNumberPosition::TopCenter);
4711    }
4712
4713    #[test]
4714    fn page_number_position_display() {
4715        assert_eq!(PageNumberPosition::None.to_string(), "None");
4716        assert_eq!(PageNumberPosition::TopCenter.to_string(), "TopCenter");
4717        assert_eq!(PageNumberPosition::BottomCenter.to_string(), "BottomCenter");
4718        assert_eq!(PageNumberPosition::InsideBottom.to_string(), "InsideBottom");
4719    }
4720
4721    #[test]
4722    fn page_number_position_from_str() {
4723        assert_eq!(PageNumberPosition::from_str("None").unwrap(), PageNumberPosition::None);
4724        assert_eq!(
4725            PageNumberPosition::from_str("BOTTOM_CENTER").unwrap(),
4726            PageNumberPosition::BottomCenter
4727        );
4728        assert_eq!(
4729            PageNumberPosition::from_str("bottom-center").unwrap(),
4730            PageNumberPosition::BottomCenter
4731        );
4732        assert_eq!(PageNumberPosition::from_str("TopLeft").unwrap(), PageNumberPosition::TopLeft);
4733        assert!(PageNumberPosition::from_str("invalid").is_err());
4734    }
4735
4736    #[test]
4737    fn page_number_position_try_from_u8() {
4738        assert_eq!(PageNumberPosition::try_from(0u8).unwrap(), PageNumberPosition::None);
4739        assert_eq!(PageNumberPosition::try_from(2u8).unwrap(), PageNumberPosition::TopCenter);
4740        assert_eq!(PageNumberPosition::try_from(5u8).unwrap(), PageNumberPosition::BottomCenter);
4741        assert_eq!(PageNumberPosition::try_from(10u8).unwrap(), PageNumberPosition::InsideBottom);
4742        assert!(PageNumberPosition::try_from(11u8).is_err());
4743    }
4744
4745    #[test]
4746    fn page_number_position_str_roundtrip() {
4747        for v in &[
4748            PageNumberPosition::None,
4749            PageNumberPosition::TopLeft,
4750            PageNumberPosition::TopCenter,
4751            PageNumberPosition::TopRight,
4752            PageNumberPosition::BottomLeft,
4753            PageNumberPosition::BottomCenter,
4754            PageNumberPosition::BottomRight,
4755            PageNumberPosition::OutsideTop,
4756            PageNumberPosition::OutsideBottom,
4757            PageNumberPosition::InsideTop,
4758            PageNumberPosition::InsideBottom,
4759        ] {
4760            let s = v.to_string();
4761            let back = PageNumberPosition::from_str(&s).unwrap();
4762            assert_eq!(&back, v);
4763        }
4764    }
4765
4766    // ===================================================================
4767    // WordBreakType
4768    // ===================================================================
4769
4770    #[test]
4771    fn word_break_type_default_is_keep_word() {
4772        assert_eq!(WordBreakType::default(), WordBreakType::KeepWord);
4773    }
4774
4775    #[test]
4776    fn word_break_type_display() {
4777        assert_eq!(WordBreakType::KeepWord.to_string(), "KEEP_WORD");
4778        assert_eq!(WordBreakType::BreakWord.to_string(), "BREAK_WORD");
4779        assert_eq!(WordBreakType::Hyphenation.to_string(), "HYPHENATION");
4780    }
4781
4782    #[test]
4783    fn word_break_type_from_str() {
4784        assert_eq!(WordBreakType::from_str("KEEP_WORD").unwrap(), WordBreakType::KeepWord);
4785        assert_eq!(WordBreakType::from_str("KeepWord").unwrap(), WordBreakType::KeepWord);
4786        assert_eq!(WordBreakType::from_str("keep_word").unwrap(), WordBreakType::KeepWord);
4787        assert_eq!(WordBreakType::from_str("BREAK_WORD").unwrap(), WordBreakType::BreakWord);
4788        assert_eq!(WordBreakType::from_str("BreakWord").unwrap(), WordBreakType::BreakWord);
4789        assert_eq!(WordBreakType::from_str("break_word").unwrap(), WordBreakType::BreakWord);
4790        assert_eq!(WordBreakType::from_str("HYPHENATION").unwrap(), WordBreakType::Hyphenation);
4791        assert_eq!(WordBreakType::from_str("Hyphenation").unwrap(), WordBreakType::Hyphenation);
4792        assert_eq!(WordBreakType::from_str("hyphenation").unwrap(), WordBreakType::Hyphenation);
4793        assert!(WordBreakType::from_str("invalid").is_err());
4794    }
4795
4796    #[test]
4797    fn word_break_type_try_from_u8() {
4798        assert_eq!(WordBreakType::try_from(0u8).unwrap(), WordBreakType::KeepWord);
4799        assert_eq!(WordBreakType::try_from(1u8).unwrap(), WordBreakType::BreakWord);
4800        assert_eq!(WordBreakType::try_from(2u8).unwrap(), WordBreakType::Hyphenation);
4801        assert!(WordBreakType::try_from(3u8).is_err());
4802    }
4803
4804    #[test]
4805    fn word_break_type_serde_roundtrip() {
4806        for v in &[WordBreakType::KeepWord, WordBreakType::BreakWord, WordBreakType::Hyphenation] {
4807            let json = serde_json::to_string(v).unwrap();
4808            let back: WordBreakType = serde_json::from_str(&json).unwrap();
4809            assert_eq!(&back, v);
4810        }
4811    }
4812
4813    #[test]
4814    fn word_break_type_str_roundtrip() {
4815        for v in &[WordBreakType::KeepWord, WordBreakType::BreakWord, WordBreakType::Hyphenation] {
4816            let s = v.to_string();
4817            let back = WordBreakType::from_str(&s).unwrap();
4818            assert_eq!(&back, v);
4819        }
4820    }
4821
4822    // ===================================================================
4823    // EmphasisType
4824    // ===================================================================
4825
4826    #[test]
4827    fn emphasis_type_default_is_none() {
4828        assert_eq!(EmphasisType::default(), EmphasisType::None);
4829    }
4830
4831    #[test]
4832    fn emphasis_type_display_pascal_case() {
4833        assert_eq!(EmphasisType::None.to_string(), "None");
4834        assert_eq!(EmphasisType::DotAbove.to_string(), "DotAbove");
4835        assert_eq!(EmphasisType::RingAbove.to_string(), "RingAbove");
4836        assert_eq!(EmphasisType::Tilde.to_string(), "Tilde");
4837        assert_eq!(EmphasisType::Caron.to_string(), "Caron");
4838        assert_eq!(EmphasisType::Side.to_string(), "Side");
4839        assert_eq!(EmphasisType::Colon.to_string(), "Colon");
4840        assert_eq!(EmphasisType::GraveAccent.to_string(), "GraveAccent");
4841        assert_eq!(EmphasisType::AcuteAccent.to_string(), "AcuteAccent");
4842        assert_eq!(EmphasisType::Circumflex.to_string(), "Circumflex");
4843        assert_eq!(EmphasisType::Macron.to_string(), "Macron");
4844        assert_eq!(EmphasisType::HookAbove.to_string(), "HookAbove");
4845        assert_eq!(EmphasisType::DotBelow.to_string(), "DotBelow");
4846    }
4847
4848    #[test]
4849    fn emphasis_type_from_str_screaming_snake_case() {
4850        assert_eq!(EmphasisType::from_str("NONE").unwrap(), EmphasisType::None);
4851        assert_eq!(EmphasisType::from_str("DOT_ABOVE").unwrap(), EmphasisType::DotAbove);
4852        assert_eq!(EmphasisType::from_str("RING_ABOVE").unwrap(), EmphasisType::RingAbove);
4853        assert_eq!(EmphasisType::from_str("GRAVE_ACCENT").unwrap(), EmphasisType::GraveAccent);
4854        assert_eq!(EmphasisType::from_str("DOT_BELOW").unwrap(), EmphasisType::DotBelow);
4855    }
4856
4857    #[test]
4858    fn emphasis_type_from_str_pascal_case() {
4859        assert_eq!(EmphasisType::from_str("None").unwrap(), EmphasisType::None);
4860        assert_eq!(EmphasisType::from_str("DotAbove").unwrap(), EmphasisType::DotAbove);
4861        assert_eq!(EmphasisType::from_str("HookAbove").unwrap(), EmphasisType::HookAbove);
4862    }
4863
4864    #[test]
4865    fn emphasis_type_from_str_invalid() {
4866        let err = EmphasisType::from_str("INVALID").unwrap_err();
4867        match err {
4868            FoundationError::ParseError { ref type_name, ref value, .. } => {
4869                assert_eq!(type_name, "EmphasisType");
4870                assert_eq!(value, "INVALID");
4871            }
4872            other => panic!("unexpected: {other}"),
4873        }
4874    }
4875
4876    #[test]
4877    fn emphasis_type_try_from_u8() {
4878        assert_eq!(EmphasisType::try_from(0u8).unwrap(), EmphasisType::None);
4879        assert_eq!(EmphasisType::try_from(1u8).unwrap(), EmphasisType::DotAbove);
4880        assert_eq!(EmphasisType::try_from(12u8).unwrap(), EmphasisType::DotBelow);
4881        assert!(EmphasisType::try_from(13u8).is_err());
4882        assert!(EmphasisType::try_from(255u8).is_err());
4883    }
4884
4885    #[test]
4886    fn emphasis_type_repr_values() {
4887        assert_eq!(EmphasisType::None as u8, 0);
4888        assert_eq!(EmphasisType::DotAbove as u8, 1);
4889        assert_eq!(EmphasisType::DotBelow as u8, 12);
4890    }
4891
4892    #[test]
4893    fn emphasis_type_serde_roundtrip() {
4894        for variant in &[
4895            EmphasisType::None,
4896            EmphasisType::DotAbove,
4897            EmphasisType::RingAbove,
4898            EmphasisType::DotBelow,
4899        ] {
4900            let json = serde_json::to_string(variant).unwrap();
4901            let back: EmphasisType = serde_json::from_str(&json).unwrap();
4902            assert_eq!(&back, variant);
4903        }
4904    }
4905
4906    #[test]
4907    fn emphasis_type_str_roundtrip() {
4908        for variant in &[
4909            EmphasisType::None,
4910            EmphasisType::DotAbove,
4911            EmphasisType::GraveAccent,
4912            EmphasisType::DotBelow,
4913        ] {
4914            let s = variant.to_string();
4915            let back = EmphasisType::from_str(&s).unwrap();
4916            assert_eq!(&back, variant);
4917        }
4918    }
4919}