Skip to main content

hwpforge_foundation/enums/
paragraph.rs

1//! Paragraph-level enums: alignment, line spacing, breaks, headings, and direction.
2
3use crate::error::FoundationError;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7// ---------------------------------------------------------------------------
8// Alignment
9// ---------------------------------------------------------------------------
10
11/// Horizontal text alignment within a paragraph.
12///
13/// # Examples
14///
15/// ```
16/// use hwpforge_foundation::Alignment;
17///
18/// assert_eq!(Alignment::default(), Alignment::Left);
19/// ```
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
21#[non_exhaustive]
22#[repr(u8)]
23pub enum Alignment {
24    /// Left-aligned (default).
25    #[default]
26    Left = 0,
27    /// Centered.
28    Center = 1,
29    /// Right-aligned.
30    Right = 2,
31    /// Justified (both edges flush).
32    Justify = 3,
33    /// Distribute spacing evenly between characters.
34    Distribute = 4,
35    /// Distribute spacing evenly between characters, last line flush.
36    DistributeFlush = 5,
37}
38
39impl fmt::Display for Alignment {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Self::Left => f.write_str("Left"),
43            Self::Center => f.write_str("Center"),
44            Self::Right => f.write_str("Right"),
45            Self::Justify => f.write_str("Justify"),
46            Self::Distribute => f.write_str("Distribute"),
47            Self::DistributeFlush => f.write_str("DistributeFlush"),
48        }
49    }
50}
51
52impl std::str::FromStr for Alignment {
53    type Err = FoundationError;
54
55    fn from_str(s: &str) -> Result<Self, Self::Err> {
56        match s {
57            "Left" | "left" => Ok(Self::Left),
58            "Center" | "center" => Ok(Self::Center),
59            "Right" | "right" => Ok(Self::Right),
60            "Justify" | "justify" => Ok(Self::Justify),
61            "Distribute" | "distribute" => Ok(Self::Distribute),
62            "DistributeFlush" | "distributeflush" | "distribute_flush" => Ok(Self::DistributeFlush),
63            _ => Err(FoundationError::ParseError {
64                type_name: "Alignment".to_string(),
65                value: s.to_string(),
66                valid_values: "Left, Center, Right, Justify, Distribute, DistributeFlush"
67                    .to_string(),
68            }),
69        }
70    }
71}
72
73impl TryFrom<u8> for Alignment {
74    type Error = FoundationError;
75
76    fn try_from(value: u8) -> Result<Self, Self::Error> {
77        match value {
78            0 => Ok(Self::Left),
79            1 => Ok(Self::Center),
80            2 => Ok(Self::Right),
81            3 => Ok(Self::Justify),
82            4 => Ok(Self::Distribute),
83            5 => Ok(Self::DistributeFlush),
84            _ => Err(FoundationError::ParseError {
85                type_name: "Alignment".to_string(),
86                value: value.to_string(),
87                valid_values:
88                    "0 (Left), 1 (Center), 2 (Right), 3 (Justify), 4 (Distribute), 5 (DistributeFlush)"
89                        .to_string(),
90            }),
91        }
92    }
93}
94
95impl schemars::JsonSchema for Alignment {
96    fn schema_name() -> std::borrow::Cow<'static, str> {
97        std::borrow::Cow::Borrowed("Alignment")
98    }
99
100    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
101        gen.subschema_for::<String>()
102    }
103}
104
105// ---------------------------------------------------------------------------
106// LineSpacingType
107// ---------------------------------------------------------------------------
108
109/// How line spacing is calculated.
110///
111/// # Examples
112///
113/// ```
114/// use hwpforge_foundation::LineSpacingType;
115///
116/// assert_eq!(LineSpacingType::default(), LineSpacingType::Percentage);
117/// ```
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
119#[non_exhaustive]
120#[repr(u8)]
121pub enum LineSpacingType {
122    /// Spacing as a percentage of the font size (default: 160%).
123    #[default]
124    Percentage = 0,
125    /// Fixed spacing in HwpUnit, regardless of font size.
126    Fixed = 1,
127    /// Space between the bottom of one line and top of the next.
128    BetweenLines = 2,
129    /// Minimum spacing in HwpUnit; line height expands when content
130    /// requires more room (HWPX wire form `AT_LEAST`).
131    AtLeast = 3,
132}
133
134impl fmt::Display for LineSpacingType {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        match self {
137            Self::Percentage => f.write_str("Percentage"),
138            Self::Fixed => f.write_str("Fixed"),
139            Self::BetweenLines => f.write_str("BetweenLines"),
140            Self::AtLeast => f.write_str("AtLeast"),
141        }
142    }
143}
144
145impl std::str::FromStr for LineSpacingType {
146    type Err = FoundationError;
147
148    fn from_str(s: &str) -> Result<Self, Self::Err> {
149        match s {
150            "Percentage" | "percentage" => Ok(Self::Percentage),
151            "Fixed" | "fixed" => Ok(Self::Fixed),
152            "BetweenLines" | "betweenlines" | "between_lines" => Ok(Self::BetweenLines),
153            "AtLeast" | "atleast" | "at_least" => Ok(Self::AtLeast),
154            _ => Err(FoundationError::ParseError {
155                type_name: "LineSpacingType".to_string(),
156                value: s.to_string(),
157                valid_values: "Percentage, Fixed, BetweenLines, AtLeast".to_string(),
158            }),
159        }
160    }
161}
162
163impl TryFrom<u8> for LineSpacingType {
164    type Error = FoundationError;
165
166    fn try_from(value: u8) -> Result<Self, Self::Error> {
167        match value {
168            0 => Ok(Self::Percentage),
169            1 => Ok(Self::Fixed),
170            2 => Ok(Self::BetweenLines),
171            3 => Ok(Self::AtLeast),
172            _ => Err(FoundationError::ParseError {
173                type_name: "LineSpacingType".to_string(),
174                value: value.to_string(),
175                valid_values: "0 (Percentage), 1 (Fixed), 2 (BetweenLines), 3 (AtLeast)"
176                    .to_string(),
177            }),
178        }
179    }
180}
181
182impl schemars::JsonSchema for LineSpacingType {
183    fn schema_name() -> std::borrow::Cow<'static, str> {
184        std::borrow::Cow::Borrowed("LineSpacingType")
185    }
186
187    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
188        gen.subschema_for::<String>()
189    }
190}
191
192// ---------------------------------------------------------------------------
193// BreakType
194// ---------------------------------------------------------------------------
195
196/// Page/column break type before a paragraph.
197///
198/// # Examples
199///
200/// ```
201/// use hwpforge_foundation::BreakType;
202///
203/// assert_eq!(BreakType::default(), BreakType::None);
204/// ```
205#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
206#[non_exhaustive]
207#[repr(u8)]
208pub enum BreakType {
209    /// No break.
210    #[default]
211    None = 0,
212    /// Column break.
213    Column = 1,
214    /// Page break.
215    Page = 2,
216}
217
218impl fmt::Display for BreakType {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        match self {
221            Self::None => f.write_str("None"),
222            Self::Column => f.write_str("Column"),
223            Self::Page => f.write_str("Page"),
224        }
225    }
226}
227
228impl std::str::FromStr for BreakType {
229    type Err = FoundationError;
230
231    fn from_str(s: &str) -> Result<Self, Self::Err> {
232        match s {
233            "None" | "none" => Ok(Self::None),
234            "Column" | "column" => Ok(Self::Column),
235            "Page" | "page" => Ok(Self::Page),
236            _ => Err(FoundationError::ParseError {
237                type_name: "BreakType".to_string(),
238                value: s.to_string(),
239                valid_values: "None, Column, Page".to_string(),
240            }),
241        }
242    }
243}
244
245impl TryFrom<u8> for BreakType {
246    type Error = FoundationError;
247
248    fn try_from(value: u8) -> Result<Self, Self::Error> {
249        match value {
250            0 => Ok(Self::None),
251            1 => Ok(Self::Column),
252            2 => Ok(Self::Page),
253            _ => Err(FoundationError::ParseError {
254                type_name: "BreakType".to_string(),
255                value: value.to_string(),
256                valid_values: "0 (None), 1 (Column), 2 (Page)".to_string(),
257            }),
258        }
259    }
260}
261
262impl schemars::JsonSchema for BreakType {
263    fn schema_name() -> std::borrow::Cow<'static, str> {
264        std::borrow::Cow::Borrowed("BreakType")
265    }
266
267    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
268        gen.subschema_for::<String>()
269    }
270}
271
272// ---------------------------------------------------------------------------
273// WordBreakType
274// ---------------------------------------------------------------------------
275
276/// Word-breaking behavior for paragraph text justification.
277///
278/// Controls how 한글 distributes extra space in justified text.
279/// `KeepWord` preserves word boundaries (natural spacing),
280/// `BreakWord` allows breaking at any character (stretched spacing).
281///
282/// # Examples
283///
284/// ```
285/// use hwpforge_foundation::WordBreakType;
286///
287/// assert_eq!(WordBreakType::default(), WordBreakType::KeepWord);
288/// assert_eq!(WordBreakType::KeepWord.to_string(), "KEEP_WORD");
289/// ```
290#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
291#[non_exhaustive]
292#[repr(u8)]
293pub enum WordBreakType {
294    /// Keep words intact — distribute space between words only (한글 default).
295    #[default]
296    KeepWord = 0,
297    /// Allow breaking at any character — distribute space between all characters.
298    BreakWord = 1,
299    /// Allow hyphenation at line breaks (Latin scripts only).
300    Hyphenation = 2,
301}
302
303impl fmt::Display for WordBreakType {
304    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305        match self {
306            Self::KeepWord => f.write_str("KEEP_WORD"),
307            Self::BreakWord => f.write_str("BREAK_WORD"),
308            Self::Hyphenation => f.write_str("HYPHENATION"),
309        }
310    }
311}
312
313impl std::str::FromStr for WordBreakType {
314    type Err = FoundationError;
315
316    fn from_str(s: &str) -> Result<Self, Self::Err> {
317        match s {
318            "KEEP_WORD" | "KeepWord" | "keep_word" => Ok(Self::KeepWord),
319            "BREAK_WORD" | "BreakWord" | "break_word" => Ok(Self::BreakWord),
320            "HYPHENATION" | "Hyphenation" | "hyphenation" => Ok(Self::Hyphenation),
321            _ => Err(FoundationError::ParseError {
322                type_name: "WordBreakType".to_string(),
323                value: s.to_string(),
324                valid_values: "KEEP_WORD, BREAK_WORD, HYPHENATION".to_string(),
325            }),
326        }
327    }
328}
329
330impl TryFrom<u8> for WordBreakType {
331    type Error = FoundationError;
332
333    fn try_from(value: u8) -> Result<Self, Self::Error> {
334        match value {
335            0 => Ok(Self::KeepWord),
336            1 => Ok(Self::BreakWord),
337            2 => Ok(Self::Hyphenation),
338            _ => Err(FoundationError::ParseError {
339                type_name: "WordBreakType".to_string(),
340                value: value.to_string(),
341                valid_values: "0 (KeepWord), 1 (BreakWord), 2 (Hyphenation)".to_string(),
342            }),
343        }
344    }
345}
346
347impl schemars::JsonSchema for WordBreakType {
348    fn schema_name() -> std::borrow::Cow<'static, str> {
349        std::borrow::Cow::Borrowed("WordBreakType")
350    }
351
352    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
353        gen.subschema_for::<String>()
354    }
355}
356
357// ---------------------------------------------------------------------------
358// HeadingType
359// ---------------------------------------------------------------------------
360
361/// Paragraph heading type for outline/numbering classification.
362///
363/// Controls how a paragraph participates in document outline or numbering.
364/// Maps to the HWPX `<hh:heading type="...">` attribute.
365///
366/// # Examples
367///
368/// ```
369/// use hwpforge_foundation::HeadingType;
370///
371/// assert_eq!(HeadingType::default(), HeadingType::None);
372/// ```
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
374#[non_exhaustive]
375#[repr(u8)]
376pub enum HeadingType {
377    /// No heading (body text, default).
378    #[default]
379    None = 0,
380    /// Outline heading (개요).
381    Outline = 1,
382    /// Number heading.
383    Number = 2,
384    /// Bullet heading.
385    Bullet = 3,
386}
387
388impl HeadingType {
389    /// Converts to the HWPX XML attribute string.
390    pub fn to_hwpx_str(self) -> &'static str {
391        match self {
392            Self::None => "NONE",
393            Self::Outline => "OUTLINE",
394            Self::Number => "NUMBER",
395            Self::Bullet => "BULLET",
396        }
397    }
398
399    /// Parses a HWPX XML attribute string.
400    pub fn from_hwpx_str(s: &str) -> Self {
401        match s {
402            "NONE" => Self::None,
403            "OUTLINE" => Self::Outline,
404            "NUMBER" => Self::Number,
405            "BULLET" => Self::Bullet,
406            _ => Self::None,
407        }
408    }
409}
410
411impl fmt::Display for HeadingType {
412    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413        match self {
414            Self::None => f.write_str("None"),
415            Self::Outline => f.write_str("Outline"),
416            Self::Number => f.write_str("Number"),
417            Self::Bullet => f.write_str("Bullet"),
418        }
419    }
420}
421
422impl std::str::FromStr for HeadingType {
423    type Err = FoundationError;
424
425    fn from_str(s: &str) -> Result<Self, Self::Err> {
426        match s {
427            "None" | "none" | "NONE" => Ok(Self::None),
428            "Outline" | "outline" | "OUTLINE" => Ok(Self::Outline),
429            "Number" | "number" | "NUMBER" => Ok(Self::Number),
430            "Bullet" | "bullet" | "BULLET" => Ok(Self::Bullet),
431            _ => Err(FoundationError::ParseError {
432                type_name: "HeadingType".to_string(),
433                value: s.to_string(),
434                valid_values: "None, Outline, Number, Bullet".to_string(),
435            }),
436        }
437    }
438}
439
440impl TryFrom<u8> for HeadingType {
441    type Error = FoundationError;
442
443    fn try_from(value: u8) -> Result<Self, Self::Error> {
444        match value {
445            0 => Ok(Self::None),
446            1 => Ok(Self::Outline),
447            2 => Ok(Self::Number),
448            3 => Ok(Self::Bullet),
449            _ => Err(FoundationError::ParseError {
450                type_name: "HeadingType".to_string(),
451                value: value.to_string(),
452                valid_values: "0 (None), 1 (Outline), 2 (Number), 3 (Bullet)".to_string(),
453            }),
454        }
455    }
456}
457
458impl schemars::JsonSchema for HeadingType {
459    fn schema_name() -> std::borrow::Cow<'static, str> {
460        std::borrow::Cow::Borrowed("HeadingType")
461    }
462
463    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
464        gen.subschema_for::<String>()
465    }
466}
467
468// ---------------------------------------------------------------------------
469// VerticalPosition
470// ---------------------------------------------------------------------------
471
472/// Superscript/subscript position type.
473///
474/// # Examples
475///
476/// ```
477/// use hwpforge_foundation::VerticalPosition;
478///
479/// assert_eq!(VerticalPosition::default(), VerticalPosition::Normal);
480/// ```
481#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
482#[non_exhaustive]
483#[repr(u8)]
484pub enum VerticalPosition {
485    /// Normal baseline (default).
486    #[default]
487    Normal = 0,
488    /// Superscript.
489    Superscript = 1,
490    /// Subscript.
491    Subscript = 2,
492}
493
494impl fmt::Display for VerticalPosition {
495    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
496        match self {
497            Self::Normal => f.write_str("Normal"),
498            Self::Superscript => f.write_str("Superscript"),
499            Self::Subscript => f.write_str("Subscript"),
500        }
501    }
502}
503
504impl std::str::FromStr for VerticalPosition {
505    type Err = FoundationError;
506
507    fn from_str(s: &str) -> Result<Self, Self::Err> {
508        match s {
509            "Normal" | "normal" => Ok(Self::Normal),
510            "Superscript" | "superscript" | "super" => Ok(Self::Superscript),
511            "Subscript" | "subscript" | "sub" => Ok(Self::Subscript),
512            _ => Err(FoundationError::ParseError {
513                type_name: "VerticalPosition".to_string(),
514                value: s.to_string(),
515                valid_values: "Normal, Superscript, Subscript".to_string(),
516            }),
517        }
518    }
519}
520
521impl TryFrom<u8> for VerticalPosition {
522    type Error = FoundationError;
523
524    fn try_from(value: u8) -> Result<Self, Self::Error> {
525        match value {
526            0 => Ok(Self::Normal),
527            1 => Ok(Self::Superscript),
528            2 => Ok(Self::Subscript),
529            _ => Err(FoundationError::ParseError {
530                type_name: "VerticalPosition".to_string(),
531                value: value.to_string(),
532                valid_values: "0 (Normal), 1 (Superscript), 2 (Subscript)".to_string(),
533            }),
534        }
535    }
536}
537
538impl schemars::JsonSchema for VerticalPosition {
539    fn schema_name() -> std::borrow::Cow<'static, str> {
540        std::borrow::Cow::Borrowed("VerticalPosition")
541    }
542
543    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
544        gen.subschema_for::<String>()
545    }
546}
547
548// ---------------------------------------------------------------------------
549// TextDirection
550// ---------------------------------------------------------------------------
551
552/// Text writing direction for sections and sub-lists.
553///
554/// Controls whether text flows horizontally (가로쓰기) or vertically (세로쓰기).
555/// Used in `<hp:secPr textDirection="...">` and `<hp:subList textDirection="...">`.
556///
557/// # Examples
558///
559/// ```
560/// use hwpforge_foundation::TextDirection;
561///
562/// assert_eq!(TextDirection::default(), TextDirection::Horizontal);
563/// assert_eq!(TextDirection::Horizontal.to_string(), "HORIZONTAL");
564/// ```
565#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
566#[non_exhaustive]
567pub enum TextDirection {
568    /// Horizontal writing (가로쓰기) — default.
569    #[default]
570    Horizontal,
571    /// Vertical writing with Latin chars rotated 90° (세로쓰기 영문 눕힘).
572    Vertical,
573    /// Vertical writing with Latin chars upright (세로쓰기 영문 세움).
574    VerticalAll,
575}
576
577impl TextDirection {
578    /// Parses a HWPX XML attribute string (e.g. `"VERTICAL"`).
579    ///
580    /// Unknown values fall back to [`TextDirection::Horizontal`].
581    pub fn from_hwpx_str(s: &str) -> Self {
582        match s {
583            "VERTICAL" => Self::Vertical,
584            "VERTICALALL" => Self::VerticalAll,
585            _ => Self::Horizontal,
586        }
587    }
588}
589
590impl fmt::Display for TextDirection {
591    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
592        match self {
593            Self::Horizontal => f.write_str("HORIZONTAL"),
594            Self::Vertical => f.write_str("VERTICAL"),
595            Self::VerticalAll => f.write_str("VERTICALALL"),
596        }
597    }
598}
599
600impl schemars::JsonSchema for TextDirection {
601    fn schema_name() -> std::borrow::Cow<'static, str> {
602        std::borrow::Cow::Borrowed("TextDirection")
603    }
604
605    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
606        gen.subschema_for::<String>()
607    }
608}
609
610const _: () = assert!(std::mem::size_of::<TextDirection>() == 1);