1use hwpforge_blueprint::registry::StyleRegistry;
11use hwpforge_core::{NumberingDef, TabDef};
12use hwpforge_foundation::{
13 Alignment, BorderFillIndex, BreakType, CharShapeIndex, Color, EmbossType, EmphasisType,
14 EngraveType, FontIndex, HeadingType, HwpUnit, LineSpacingType, OutlineType, ParaShapeIndex,
15 ShadowType, StrikeoutShape, UnderlineType, VerticalPosition, WordBreakType,
16};
17
18use crate::default_styles::HancomStyleSet;
19use crate::error::{HwpxError, HwpxResult};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
25#[non_exhaustive]
26pub struct HwpxFont {
27 pub id: u32,
29 pub face_name: String,
31 pub lang: String,
33}
34
35impl HwpxFont {
36 pub fn new(id: u32, face_name: impl Into<String>, lang: impl Into<String>) -> Self {
38 Self { id, face_name: face_name.into(), lang: lang.into() }
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49#[non_exhaustive]
50pub struct HwpxFontRef {
51 pub hangul: FontIndex,
53 pub latin: FontIndex,
55 pub hanja: FontIndex,
57 pub japanese: FontIndex,
59 pub other: FontIndex,
61 pub symbol: FontIndex,
63 pub user: FontIndex,
65}
66
67impl Default for HwpxFontRef {
68 fn default() -> Self {
69 let zero = FontIndex::new(0);
70 Self {
71 hangul: zero,
72 latin: zero,
73 hanja: zero,
74 japanese: zero,
75 other: zero,
76 symbol: zero,
77 user: zero,
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
88#[non_exhaustive]
89pub struct HwpxCharShape {
90 pub font_ref: HwpxFontRef,
92 pub height: HwpUnit,
94 pub text_color: Color,
96 pub shade_color: Option<Color>,
98 pub bold: bool,
100 pub italic: bool,
102 pub underline_type: UnderlineType,
104 pub underline_color: Option<Color>,
106 pub strikeout_shape: StrikeoutShape,
108 pub strikeout_color: Option<Color>,
110 pub vertical_position: VerticalPosition,
112 pub outline_type: OutlineType,
114 pub shadow_type: ShadowType,
116 pub emboss_type: EmbossType,
118 pub engrave_type: EngraveType,
120 pub emphasis: EmphasisType,
122 pub ratio: i32,
124 pub spacing: i32,
126 pub rel_sz: i32,
128 pub char_offset: i32,
130 pub use_kerning: bool,
132 pub use_font_space: bool,
134 pub border_fill_id: Option<u32>,
139}
140
141impl Default for HwpxCharShape {
142 fn default() -> Self {
143 Self {
144 font_ref: HwpxFontRef::default(),
145 height: HwpUnit::new(1000).unwrap(), text_color: Color::BLACK,
147 shade_color: None,
148 bold: false,
149 italic: false,
150 underline_type: UnderlineType::None,
151 underline_color: None,
152 strikeout_shape: StrikeoutShape::None,
153 strikeout_color: None,
154 vertical_position: VerticalPosition::Normal,
155 outline_type: OutlineType::None,
156 shadow_type: ShadowType::None,
157 emboss_type: EmbossType::None,
158 engrave_type: EngraveType::None,
159 emphasis: EmphasisType::None,
160 ratio: 100,
161 spacing: 0,
162 rel_sz: 100,
163 char_offset: 0,
164 use_kerning: false,
165 use_font_space: false,
166 border_fill_id: None,
167 }
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
179#[non_exhaustive]
180pub struct HwpxStyle {
181 pub id: u32,
183 pub style_type: String,
185 pub name: String,
187 pub eng_name: String,
189 pub para_pr_id_ref: u32,
191 pub char_pr_id_ref: u32,
193 pub next_style_id_ref: u32,
195 pub lang_id: u32,
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
203#[non_exhaustive]
204pub struct HwpxParaShape {
205 pub alignment: Alignment,
207 pub margin_left: HwpUnit,
209 pub margin_right: HwpUnit,
211 pub indent: HwpUnit,
213 pub spacing_before: HwpUnit,
215 pub spacing_after: HwpUnit,
217 pub line_spacing: i32,
219 pub line_spacing_type: LineSpacingType,
221
222 pub break_type: BreakType,
225 pub keep_with_next: bool,
227 pub keep_lines_together: bool,
229 pub widow_orphan: bool,
231 pub break_latin_word: WordBreakType,
233 pub break_non_latin_word: WordBreakType,
235 pub border_fill_id: Option<BorderFillIndex>,
237 pub heading_type: HeadingType,
239 pub heading_id_ref: u32,
241 pub heading_level: u32,
243 pub tab_pr_id_ref: u32,
245 pub condense: u32,
247}
248
249impl Default for HwpxParaShape {
250 fn default() -> Self {
251 Self {
252 alignment: Alignment::Left,
253 margin_left: HwpUnit::ZERO,
254 margin_right: HwpUnit::ZERO,
255 indent: HwpUnit::ZERO,
256 spacing_before: HwpUnit::ZERO,
257 spacing_after: HwpUnit::ZERO,
258 line_spacing: 160,
259 line_spacing_type: LineSpacingType::Percentage,
260 break_type: BreakType::None,
261 keep_with_next: false,
262 keep_lines_together: false,
263 widow_orphan: true, break_latin_word: WordBreakType::KeepWord,
265 break_non_latin_word: WordBreakType::KeepWord,
266 border_fill_id: None,
267 heading_type: HeadingType::None,
268 heading_id_ref: 0,
269 heading_level: 0,
270 tab_pr_id_ref: 0,
271 condense: 0,
272 }
273 }
274}
275
276#[derive(Debug, Clone, PartialEq)]
283#[non_exhaustive]
284pub struct HwpxBorderFill {
285 pub id: u32,
287 pub three_d: bool,
289 pub shadow: bool,
291 pub center_line: String,
293 pub left: HwpxBorderLine,
295 pub right: HwpxBorderLine,
297 pub top: HwpxBorderLine,
299 pub bottom: HwpxBorderLine,
301 pub diagonal: HwpxBorderLine,
303 pub slash_type: String,
305 pub back_slash_type: String,
307 pub fill: Option<HwpxFill>,
309}
310
311#[derive(Debug, Clone, PartialEq)]
313pub struct HwpxBorderLine {
314 pub line_type: String,
316 pub width: String,
318 pub color: String,
320}
321
322impl Default for HwpxBorderLine {
323 fn default() -> Self {
324 Self { line_type: "NONE".into(), width: "0.1 mm".into(), color: "#000000".into() }
325 }
326}
327
328#[derive(Debug, Clone, PartialEq)]
330pub enum HwpxFill {
331 WinBrush {
333 face_color: String,
335 hatch_color: String,
337 alpha: String,
339 },
340}
341
342impl HwpxBorderFill {
343 pub fn default_page_border() -> Self {
347 let none_border = HwpxBorderLine::default(); Self {
349 id: 1,
350 three_d: false,
351 shadow: false,
352 center_line: "NONE".into(),
353 left: none_border.clone(),
354 right: none_border.clone(),
355 top: none_border.clone(),
356 bottom: none_border.clone(),
357 diagonal: HwpxBorderLine { line_type: "SOLID".into(), ..HwpxBorderLine::default() },
358 slash_type: "NONE".into(),
359 back_slash_type: "NONE".into(),
360 fill: None,
361 }
362 }
363
364 pub fn default_char_background() -> Self {
369 let none_border = HwpxBorderLine::default();
370 Self {
371 id: 2,
372 three_d: false,
373 shadow: false,
374 center_line: "NONE".into(),
375 left: none_border.clone(),
376 right: none_border.clone(),
377 top: none_border.clone(),
378 bottom: none_border.clone(),
379 diagonal: HwpxBorderLine { line_type: "SOLID".into(), ..HwpxBorderLine::default() },
380 slash_type: "NONE".into(),
381 back_slash_type: "NONE".into(),
382 fill: Some(HwpxFill::WinBrush {
383 face_color: "none".into(),
384 hatch_color: "#FF000000".into(),
385 alpha: "0".into(),
386 }),
387 }
388 }
389
390 pub fn default_table_border() -> Self {
394 let solid_border = HwpxBorderLine {
395 line_type: "SOLID".into(),
396 width: "0.12 mm".into(),
397 color: "#000000".into(),
398 };
399 Self {
400 id: 3,
401 three_d: false,
402 shadow: false,
403 center_line: "NONE".into(),
404 left: solid_border.clone(),
405 right: solid_border.clone(),
406 top: solid_border.clone(),
407 bottom: solid_border.clone(),
408 diagonal: HwpxBorderLine { line_type: "SOLID".into(), ..HwpxBorderLine::default() },
409 slash_type: "NONE".into(),
410 back_slash_type: "NONE".into(),
411 fill: None,
412 }
413 }
414}
415
416pub(crate) fn default_char_shapes_modern() -> [HwpxCharShape; 7] {
434 let batang = FontIndex::new(1); let dotum = FontIndex::new(0); let batang_ref = HwpxFontRef {
438 hangul: batang,
439 latin: batang,
440 hanja: batang,
441 japanese: batang,
442 other: batang,
443 symbol: batang,
444 user: batang,
445 };
446 let dotum_ref = HwpxFontRef {
447 hangul: dotum,
448 latin: dotum,
449 hanja: dotum,
450 japanese: dotum,
451 other: dotum,
452 symbol: dotum,
453 user: dotum,
454 };
455
456 let base = HwpxCharShape {
457 font_ref: batang_ref,
458 height: HwpUnit::new(1000).unwrap(), text_color: Color::BLACK,
460 shade_color: None,
461 bold: false,
462 italic: false,
463 underline_type: UnderlineType::None,
464 underline_color: None,
465 strikeout_shape: StrikeoutShape::None,
466 strikeout_color: None,
467 vertical_position: VerticalPosition::Normal,
468 outline_type: OutlineType::None,
469 shadow_type: ShadowType::None,
470 emboss_type: EmbossType::None,
471 engrave_type: EngraveType::None,
472 emphasis: EmphasisType::None,
473 ratio: 100,
474 spacing: 0,
475 rel_sz: 100,
476 char_offset: 0,
477 use_kerning: false,
478 use_font_space: false,
479 border_fill_id: None,
480 };
481
482 [
483 base.clone(),
485 HwpxCharShape { font_ref: dotum_ref, ..base.clone() },
487 HwpxCharShape { font_ref: dotum_ref, height: HwpUnit::new(900).unwrap(), ..base.clone() },
489 HwpxCharShape { height: HwpUnit::new(900).unwrap(), ..base.clone() },
491 HwpxCharShape { font_ref: dotum_ref, height: HwpUnit::new(900).unwrap(), ..base.clone() },
493 HwpxCharShape {
495 font_ref: dotum_ref,
496 height: HwpUnit::new(1600).unwrap(),
497 text_color: Color::from_rgb(0x2E, 0x74, 0xB5),
498 ..base.clone()
499 },
500 HwpxCharShape { font_ref: dotum_ref, height: HwpUnit::new(1100).unwrap(), ..base },
502 ]
503}
504
505pub(crate) fn default_para_shapes_modern() -> [HwpxParaShape; 20] {
511 let justify = Alignment::Justify;
512 let left = Alignment::Left;
513
514 let base = HwpxParaShape {
516 alignment: justify,
517 margin_left: HwpUnit::ZERO,
518 margin_right: HwpUnit::ZERO,
519 indent: HwpUnit::ZERO,
520 spacing_before: HwpUnit::ZERO,
521 spacing_after: HwpUnit::ZERO,
522 line_spacing: 160,
523 line_spacing_type: LineSpacingType::Percentage,
524 break_type: BreakType::None,
525 keep_with_next: false,
526 keep_lines_together: false,
527 widow_orphan: false,
528 break_latin_word: WordBreakType::KeepWord,
529 break_non_latin_word: WordBreakType::KeepWord,
530 border_fill_id: None,
531 heading_type: HeadingType::None,
532 heading_id_ref: 0,
533 heading_level: 0,
534 tab_pr_id_ref: 0,
535 condense: 0,
536 };
537
538 [
539 base.clone(),
541 HwpxParaShape { margin_left: HwpUnit::new(1500).unwrap(), ..base.clone() },
543 HwpxParaShape {
545 margin_left: HwpUnit::new(1000).unwrap(),
546 heading_type: HeadingType::Outline,
547 heading_id_ref: 1,
548 heading_level: 1,
549 tab_pr_id_ref: 1,
550 condense: 20,
551 ..base.clone()
552 },
553 HwpxParaShape {
555 margin_left: HwpUnit::new(2000).unwrap(),
556 heading_type: HeadingType::Outline,
557 heading_id_ref: 1,
558 heading_level: 2,
559 tab_pr_id_ref: 1,
560 condense: 20,
561 ..base.clone()
562 },
563 HwpxParaShape {
565 margin_left: HwpUnit::new(3000).unwrap(),
566 heading_type: HeadingType::Outline,
567 heading_id_ref: 1,
568 heading_level: 3,
569 tab_pr_id_ref: 1,
570 condense: 20,
571 ..base.clone()
572 },
573 HwpxParaShape {
575 margin_left: HwpUnit::new(4000).unwrap(),
576 heading_type: HeadingType::Outline,
577 heading_id_ref: 1,
578 heading_level: 4,
579 tab_pr_id_ref: 1,
580 condense: 20,
581 ..base.clone()
582 },
583 HwpxParaShape {
585 margin_left: HwpUnit::new(5000).unwrap(),
586 heading_type: HeadingType::Outline,
587 heading_id_ref: 1,
588 heading_level: 5,
589 tab_pr_id_ref: 1,
590 condense: 20,
591 ..base.clone()
592 },
593 HwpxParaShape {
595 margin_left: HwpUnit::new(6000).unwrap(),
596 heading_type: HeadingType::Outline,
597 heading_id_ref: 1,
598 heading_level: 6,
599 tab_pr_id_ref: 1,
600 condense: 20,
601 ..base.clone()
602 },
603 HwpxParaShape {
605 margin_left: HwpUnit::new(7000).unwrap(),
606 heading_type: HeadingType::Outline,
607 heading_id_ref: 1,
608 heading_level: 7,
609 tab_pr_id_ref: 1,
610 condense: 20,
611 ..base.clone()
612 },
613 HwpxParaShape { line_spacing: 150, ..base.clone() },
615 HwpxParaShape { indent: HwpUnit::new(-1310).unwrap(), line_spacing: 130, ..base.clone() },
617 HwpxParaShape { alignment: left, line_spacing: 130, ..base.clone() },
619 HwpxParaShape {
621 alignment: left,
622 spacing_before: HwpUnit::new(1200).unwrap(),
623 spacing_after: HwpUnit::new(300).unwrap(),
624 ..base.clone()
625 },
626 HwpxParaShape {
628 alignment: left,
629 spacing_after: HwpUnit::new(700).unwrap(),
630 ..base.clone()
631 },
632 HwpxParaShape {
634 alignment: left,
635 margin_left: HwpUnit::new(1100).unwrap(),
636 spacing_after: HwpUnit::new(700).unwrap(),
637 ..base.clone()
638 },
639 HwpxParaShape {
641 alignment: left,
642 margin_left: HwpUnit::new(2200).unwrap(),
643 spacing_after: HwpUnit::new(700).unwrap(),
644 ..base.clone()
645 },
646 HwpxParaShape {
648 margin_left: HwpUnit::new(9000).unwrap(),
649 heading_type: HeadingType::Outline,
650 heading_id_ref: 1,
651 heading_level: 9,
652 tab_pr_id_ref: 1,
653 condense: 20,
654 ..base.clone()
655 },
656 HwpxParaShape {
658 margin_left: HwpUnit::new(10000).unwrap(),
659 heading_type: HeadingType::Outline,
660 heading_id_ref: 1,
661 heading_level: 10,
662 tab_pr_id_ref: 1,
663 condense: 20,
664 ..base.clone()
665 },
666 HwpxParaShape {
668 margin_left: HwpUnit::new(8000).unwrap(),
669 heading_type: HeadingType::Outline,
670 heading_id_ref: 1,
671 heading_level: 8,
672 tab_pr_id_ref: 1,
673 condense: 20,
674 ..base.clone()
675 },
676 HwpxParaShape { line_spacing: 150, spacing_after: HwpUnit::new(800).unwrap(), ..base },
678 ]
679}
680
681#[derive(Debug, Clone, Default)]
703pub struct HwpxStyleStore {
704 style_set: HancomStyleSet,
706 fonts: Vec<HwpxFont>,
707 char_shapes: Vec<HwpxCharShape>,
708 para_shapes: Vec<HwpxParaShape>,
709 styles: Vec<HwpxStyle>,
710 border_fills: Vec<HwpxBorderFill>,
711 numberings: Vec<NumberingDef>,
712 tabs: Vec<TabDef>,
713}
714
715impl HwpxStyleStore {
716 pub fn new() -> Self {
718 Self::default()
719 }
720
721 pub fn with_default_fonts(font_name: &str) -> Self {
735 let mut store: Self = Self::new();
736 let langs: [&str; 7] = ["HANGUL", "LATIN", "HANJA", "JAPANESE", "OTHER", "SYMBOL", "USER"];
737 for (idx, &lang) in langs.iter().enumerate() {
738 store.push_font(HwpxFont::new(idx as u32, font_name, lang));
739 }
740 store
741 }
742
743 pub fn style_set(&self) -> HancomStyleSet {
745 self.style_set
746 }
747
748 pub fn from_registry(registry: &StyleRegistry) -> Self {
756 Self::from_registry_with(registry, HancomStyleSet::default())
757 }
758
759 pub fn from_registry_with(registry: &StyleRegistry, style_set: HancomStyleSet) -> Self {
772 let mut store = Self { style_set, ..Self::default() };
773
774 let has_fonts = !registry.fonts.is_empty();
777 let default_font = if has_fonts {
778 registry.fonts[0].as_str()
779 } else {
780 "함초롬바탕" };
782
783 const FONT_LANGS: &[&str] =
786 &["HANGUL", "LATIN", "HANJA", "JAPANESE", "OTHER", "SYMBOL", "USER"];
787
788 if has_fonts {
789 for &lang in FONT_LANGS {
790 for (i, font_id) in registry.fonts.iter().enumerate() {
791 store.push_font(HwpxFont {
792 id: i as u32,
793 face_name: font_id.as_str().to_string(),
794 lang: lang.to_string(),
795 });
796 }
797 }
798 } else {
799 for &lang in FONT_LANGS {
801 store.push_font(HwpxFont {
802 id: 0,
803 face_name: default_font.to_string(),
804 lang: lang.to_string(),
805 });
806 }
807 }
808
809 for cs in default_char_shapes_modern() {
818 store.push_char_shape(cs);
819 }
820 for ps in default_para_shapes_modern() {
821 store.push_para_shape(ps);
822 }
823
824 let char_shape_offset = store.char_shape_count(); let para_shape_offset = store.para_shape_count(); for cs in ®istry.char_shapes {
830 let font_idx = registry
831 .fonts
832 .iter()
833 .position(|f| f.as_str() == cs.font)
834 .map(FontIndex::new)
835 .unwrap_or(FontIndex::new(0));
836 let font_ref = HwpxFontRef {
837 hangul: font_idx,
838 latin: font_idx,
839 hanja: font_idx,
840 japanese: font_idx,
841 other: font_idx,
842 symbol: font_idx,
843 user: font_idx,
844 };
845 store.push_char_shape(HwpxCharShape {
846 font_ref,
847 height: cs.size,
848 text_color: cs.color,
849 shade_color: cs.shade_color,
850 bold: cs.bold,
851 italic: cs.italic,
852 underline_type: cs.underline_type,
853 underline_color: cs.underline_color,
854 strikeout_shape: cs.strikeout_shape,
855 strikeout_color: cs.strikeout_color,
856 vertical_position: cs.vertical_position,
857 outline_type: cs.outline,
858 shadow_type: cs.shadow,
859 emboss_type: cs.emboss,
860 engrave_type: cs.engrave,
861 emphasis: cs.emphasis,
862 ratio: cs.ratio,
863 spacing: cs.spacing,
864 rel_sz: cs.rel_sz,
865 char_offset: cs.offset,
866 use_kerning: cs.use_kerning,
867 use_font_space: cs.use_font_space,
868 border_fill_id: cs.char_border_fill_id,
869 });
870 }
871
872 for ps in ®istry.para_shapes {
874 store.push_para_shape(HwpxParaShape {
875 alignment: ps.alignment,
876 margin_left: ps.indent_left,
877 margin_right: ps.indent_right,
878 indent: ps.indent_first_line,
879 spacing_before: ps.space_before,
880 spacing_after: ps.space_after,
881 line_spacing: ps.line_spacing_value.round() as i32,
882 line_spacing_type: ps.line_spacing_type,
883 break_type: ps.break_type,
884 keep_with_next: ps.keep_with_next,
885 keep_lines_together: ps.keep_lines_together,
886 widow_orphan: ps.widow_orphan,
887 break_latin_word: WordBreakType::KeepWord,
888 break_non_latin_word: WordBreakType::KeepWord,
889 border_fill_id: ps.border_fill_id,
890 heading_type: HeadingType::None,
891 heading_id_ref: 0,
892 heading_level: 0,
893 tab_pr_id_ref: 0,
894 condense: 0,
895 });
896 }
897
898 store.push_border_fill(HwpxBorderFill::default_page_border()); store.push_border_fill(HwpxBorderFill::default_char_background()); store.push_border_fill(HwpxBorderFill::default_table_border()); let defaults = store.style_set.default_styles();
907 for (idx, entry) in defaults.iter().enumerate() {
908 let next_style_id_ref = if entry.is_char_style() { 0 } else { idx as u32 };
909 store.push_style(HwpxStyle {
910 id: idx as u32,
911 style_type: entry.style_type.to_string(),
912 name: entry.name.to_string(),
913 eng_name: entry.eng_name.to_string(),
914 para_pr_id_ref: entry.para_pr_group as u32,
915 char_pr_id_ref: entry.char_pr_group as u32,
916 next_style_id_ref,
917 lang_id: 1042, });
919 }
920
921 let style_offset = defaults.len();
925 for (i, (name, entry)) in registry.style_entries.iter().enumerate() {
926 store.push_style(HwpxStyle {
927 id: (style_offset + i) as u32,
928 style_type: "PARA".to_string(),
929 name: name.clone(),
930 eng_name: name.clone(),
931 para_pr_id_ref: (entry.para_shape_id.get() + para_shape_offset) as u32,
932 char_pr_id_ref: (entry.char_shape_id.get() + char_shape_offset) as u32,
933 next_style_id_ref: 0,
934 lang_id: 1042, });
936 }
937
938 store
939 }
940
941 pub fn push_font(&mut self, font: HwpxFont) -> FontIndex {
945 let idx = FontIndex::new(self.fonts.len());
946 self.fonts.push(font);
947 idx
948 }
949
950 pub fn font(&self, index: FontIndex) -> HwpxResult<&HwpxFont> {
952 self.fonts.get(index.get()).ok_or_else(|| HwpxError::IndexOutOfBounds {
953 kind: "font",
954 index: index.get() as u32,
955 max: self.fonts.len() as u32,
956 })
957 }
958
959 pub fn font_count(&self) -> usize {
961 self.fonts.len()
962 }
963
964 pub fn push_char_shape(&mut self, shape: HwpxCharShape) -> CharShapeIndex {
968 let idx = CharShapeIndex::new(self.char_shapes.len());
969 self.char_shapes.push(shape);
970 idx
971 }
972
973 pub fn char_shape(&self, index: CharShapeIndex) -> HwpxResult<&HwpxCharShape> {
975 self.char_shapes.get(index.get()).ok_or_else(|| HwpxError::IndexOutOfBounds {
976 kind: "char_shape",
977 index: index.get() as u32,
978 max: self.char_shapes.len() as u32,
979 })
980 }
981
982 pub fn char_shape_count(&self) -> usize {
984 self.char_shapes.len()
985 }
986
987 pub fn push_para_shape(&mut self, shape: HwpxParaShape) -> ParaShapeIndex {
991 let idx = ParaShapeIndex::new(self.para_shapes.len());
992 self.para_shapes.push(shape);
993 idx
994 }
995
996 pub fn para_shape(&self, index: ParaShapeIndex) -> HwpxResult<&HwpxParaShape> {
998 self.para_shapes.get(index.get()).ok_or_else(|| HwpxError::IndexOutOfBounds {
999 kind: "para_shape",
1000 index: index.get() as u32,
1001 max: self.para_shapes.len() as u32,
1002 })
1003 }
1004
1005 pub fn para_shape_count(&self) -> usize {
1007 self.para_shapes.len()
1008 }
1009
1010 pub fn iter_fonts(&self) -> impl Iterator<Item = &HwpxFont> {
1014 self.fonts.iter()
1015 }
1016
1017 pub fn iter_char_shapes(&self) -> impl Iterator<Item = &HwpxCharShape> {
1019 self.char_shapes.iter()
1020 }
1021
1022 pub fn iter_para_shapes(&self) -> impl Iterator<Item = &HwpxParaShape> {
1024 self.para_shapes.iter()
1025 }
1026
1027 pub fn push_style(&mut self, style: HwpxStyle) {
1031 self.styles.push(style);
1032 }
1033
1034 pub fn style(&self, index: usize) -> HwpxResult<&HwpxStyle> {
1036 self.styles.get(index).ok_or(HwpxError::IndexOutOfBounds {
1037 kind: "style",
1038 index: index as u32,
1039 max: self.styles.len() as u32,
1040 })
1041 }
1042
1043 pub fn style_count(&self) -> usize {
1045 self.styles.len()
1046 }
1047
1048 pub fn iter_styles(&self) -> impl Iterator<Item = &HwpxStyle> {
1050 self.styles.iter()
1051 }
1052
1053 pub fn push_border_fill(&mut self, bf: HwpxBorderFill) -> u32 {
1059 let id = bf.id;
1060 self.border_fills.push(bf);
1061 id
1062 }
1063
1064 pub fn border_fill(&self, id: u32) -> HwpxResult<&HwpxBorderFill> {
1070 self.border_fills.iter().find(|bf| bf.id == id).ok_or(HwpxError::IndexOutOfBounds {
1071 kind: "border_fill",
1072 index: id,
1073 max: self.border_fills.len() as u32,
1074 })
1075 }
1076
1077 pub fn border_fill_count(&self) -> usize {
1079 self.border_fills.len()
1080 }
1081
1082 pub fn iter_border_fills(&self) -> impl Iterator<Item = &HwpxBorderFill> {
1084 self.border_fills.iter()
1085 }
1086
1087 pub fn push_numbering(&mut self, ndef: NumberingDef) {
1089 self.numberings.push(ndef);
1090 }
1091
1092 pub fn push_tab(&mut self, tab: TabDef) {
1094 self.tabs.push(tab);
1095 }
1096
1097 pub fn numbering_count(&self) -> u32 {
1099 self.numberings.len() as u32
1100 }
1101
1102 pub fn tab_count(&self) -> u32 {
1104 self.tabs.len() as u32
1105 }
1106
1107 pub fn iter_numberings(&self) -> impl Iterator<Item = &NumberingDef> {
1109 self.numberings.iter()
1110 }
1111
1112 pub fn iter_tabs(&self) -> impl Iterator<Item = &TabDef> {
1114 self.tabs.iter()
1115 }
1116}
1117
1118#[allow(dead_code)]
1121const _: () = {
1122 fn assert_send<T: Send>() {}
1123 fn assert_sync<T: Sync>() {}
1124 fn assertions() {
1125 assert_send::<HwpxStyleStore>();
1126 assert_sync::<HwpxStyleStore>();
1127 }
1128};
1129
1130pub(crate) fn parse_hex_color(s: &str) -> Color {
1139 let s = s.trim();
1140 if s.is_empty() || s.eq_ignore_ascii_case("none") {
1141 return Color::BLACK;
1142 }
1143 let hex = s.strip_prefix('#').unwrap_or(s);
1144 if hex.len() != 6 {
1145 return Color::BLACK;
1146 }
1147 let Ok(rgb) = u32::from_str_radix(hex, 16) else {
1148 return Color::BLACK;
1149 };
1150 let r = ((rgb >> 16) & 0xFF) as u8;
1151 let g = ((rgb >> 8) & 0xFF) as u8;
1152 let b = (rgb & 0xFF) as u8;
1153 Color::from_rgb(r, g, b)
1154}
1155
1156pub(crate) fn parse_alignment(s: &str) -> Alignment {
1160 if s.eq_ignore_ascii_case("LEFT") {
1161 Alignment::Left
1162 } else if s.eq_ignore_ascii_case("BOTH") || s.eq_ignore_ascii_case("JUSTIFY") {
1163 Alignment::Justify
1164 } else if s.eq_ignore_ascii_case("CENTER") {
1165 Alignment::Center
1166 } else if s.eq_ignore_ascii_case("RIGHT") {
1167 Alignment::Right
1168 } else if s.eq_ignore_ascii_case("DISTRIBUTE") {
1169 Alignment::Distribute
1170 } else if s.eq_ignore_ascii_case("DISTRIBUTE_FLUSH") {
1171 Alignment::DistributeFlush
1172 } else {
1173 Alignment::Left
1174 }
1175}
1176
1177#[cfg(test)]
1178mod tests {
1179 use super::*;
1180 use hwpforge_blueprint::builtins::builtin_default;
1181 use hwpforge_foundation::{CharShapeIndex, FontIndex, ParaShapeIndex};
1182
1183 #[test]
1186 fn empty_store_returns_errors() {
1187 let store = HwpxStyleStore::new();
1188 assert!(store.font(FontIndex::new(0)).is_err());
1189 assert!(store.char_shape(CharShapeIndex::new(0)).is_err());
1190 assert!(store.para_shape(ParaShapeIndex::new(0)).is_err());
1191 }
1192
1193 #[test]
1194 fn push_and_get_font() {
1195 let mut store = HwpxStyleStore::new();
1196 let idx = store.push_font(HwpxFont {
1197 id: 0,
1198 face_name: "함초롬돋움".into(),
1199 lang: "HANGUL".into(),
1200 });
1201 assert_eq!(idx.get(), 0);
1202 let font = store.font(idx).unwrap();
1203 assert_eq!(font.face_name, "함초롬돋움");
1204 assert_eq!(font.lang, "HANGUL");
1205 }
1206
1207 #[test]
1208 fn push_and_get_char_shape() {
1209 let mut store = HwpxStyleStore::new();
1210 let shape = HwpxCharShape {
1211 height: HwpUnit::new(1000).unwrap(),
1212 text_color: Color::from_rgb(255, 0, 0),
1213 bold: true,
1214 ..Default::default()
1215 };
1216 let idx = store.push_char_shape(shape);
1217 let cs = store.char_shape(idx).unwrap();
1218 assert_eq!(cs.height.as_i32(), 1000);
1219 assert_eq!(cs.text_color.red(), 255);
1220 assert!(cs.bold);
1221 assert!(!cs.italic);
1222 }
1223
1224 #[test]
1225 fn push_and_get_para_shape() {
1226 let mut store = HwpxStyleStore::new();
1227 let shape =
1228 HwpxParaShape { alignment: Alignment::Center, line_spacing: 200, ..Default::default() };
1229 let idx = store.push_para_shape(shape);
1230 let ps = store.para_shape(idx).unwrap();
1231 assert_eq!(ps.alignment, Alignment::Center);
1232 assert_eq!(ps.line_spacing, 200);
1233 }
1234
1235 #[test]
1236 fn index_out_of_bounds_error() {
1237 let store = HwpxStyleStore::new();
1238 let err = store.char_shape(CharShapeIndex::new(42)).unwrap_err();
1239 match err {
1240 HwpxError::IndexOutOfBounds { kind, index, max } => {
1241 assert_eq!(kind, "char_shape");
1242 assert_eq!(index, 42);
1243 assert_eq!(max, 0);
1244 }
1245 _ => panic!("expected IndexOutOfBounds"),
1246 }
1247 }
1248
1249 #[test]
1250 fn multiple_items_sequential_indices() {
1251 let mut store = HwpxStyleStore::new();
1252 for i in 0..5 {
1253 let idx = store.push_font(HwpxFont {
1254 id: i,
1255 face_name: format!("Font{i}"),
1256 lang: "LATIN".into(),
1257 });
1258 assert_eq!(idx.get(), i as usize);
1259 }
1260 assert_eq!(store.font_count(), 5);
1261 assert_eq!(store.font(FontIndex::new(3)).unwrap().face_name, "Font3");
1262 }
1263
1264 #[test]
1265 fn count_methods() {
1266 let mut store = HwpxStyleStore::new();
1267 assert_eq!(store.font_count(), 0);
1268 assert_eq!(store.char_shape_count(), 0);
1269 assert_eq!(store.para_shape_count(), 0);
1270
1271 store.push_font(HwpxFont { id: 0, face_name: "A".into(), lang: "LATIN".into() });
1272 store.push_char_shape(HwpxCharShape::default());
1273 store.push_char_shape(HwpxCharShape::default());
1274 store.push_para_shape(HwpxParaShape::default());
1275
1276 assert_eq!(store.font_count(), 1);
1277 assert_eq!(store.char_shape_count(), 2);
1278 assert_eq!(store.para_shape_count(), 1);
1279 }
1280
1281 #[test]
1284 fn iter_fonts_yields_all() {
1285 let mut store = HwpxStyleStore::new();
1286 for i in 0..3 {
1287 store.push_font(HwpxFont {
1288 id: i,
1289 face_name: format!("Font{i}"),
1290 lang: "LATIN".into(),
1291 });
1292 }
1293 let names: Vec<&str> = store.iter_fonts().map(|f| f.face_name.as_str()).collect();
1294 assert_eq!(names, vec!["Font0", "Font1", "Font2"]);
1295 }
1296
1297 #[test]
1298 fn iter_char_shapes_yields_all() {
1299 let mut store = HwpxStyleStore::new();
1300 store.push_char_shape(HwpxCharShape { bold: true, ..Default::default() });
1301 store.push_char_shape(HwpxCharShape { italic: true, ..Default::default() });
1302 let styles: Vec<(bool, bool)> =
1303 store.iter_char_shapes().map(|c| (c.bold, c.italic)).collect();
1304 assert_eq!(styles, vec![(true, false), (false, true)]);
1305 }
1306
1307 #[test]
1308 fn iter_para_shapes_yields_all() {
1309 let mut store = HwpxStyleStore::new();
1310 store.push_para_shape(HwpxParaShape { line_spacing: 130, ..Default::default() });
1311 store.push_para_shape(HwpxParaShape { line_spacing: 200, ..Default::default() });
1312 let spacings: Vec<i32> = store.iter_para_shapes().map(|p| p.line_spacing).collect();
1313 assert_eq!(spacings, vec![130, 200]);
1314 }
1315
1316 #[test]
1317 fn iter_empty_store() {
1318 let store = HwpxStyleStore::new();
1319 assert_eq!(store.iter_fonts().count(), 0);
1320 assert_eq!(store.iter_char_shapes().count(), 0);
1321 assert_eq!(store.iter_para_shapes().count(), 0);
1322 }
1323
1324 #[test]
1327 fn font_ref_default_all_zero() {
1328 let r = HwpxFontRef::default();
1329 assert_eq!(r.hangul.get(), 0);
1330 assert_eq!(r.latin.get(), 0);
1331 assert_eq!(r.hanja.get(), 0);
1332 assert_eq!(r.japanese.get(), 0);
1333 assert_eq!(r.other.get(), 0);
1334 assert_eq!(r.symbol.get(), 0);
1335 assert_eq!(r.user.get(), 0);
1336 }
1337
1338 #[test]
1341 fn char_shape_default_values() {
1342 let cs = HwpxCharShape::default();
1343 assert_eq!(cs.height, HwpUnit::new(1000).unwrap()); assert_eq!(cs.text_color, Color::BLACK);
1345 assert_eq!(cs.shade_color, None);
1346 assert!(!cs.bold);
1347 assert!(!cs.italic);
1348 assert_eq!(cs.underline_type, UnderlineType::None);
1349 assert_eq!(cs.underline_color, None);
1350 assert_eq!(cs.strikeout_shape, StrikeoutShape::None);
1351 assert_eq!(cs.strikeout_color, None);
1352 }
1353
1354 #[test]
1357 fn para_shape_default_values() {
1358 let ps = HwpxParaShape::default();
1359 assert_eq!(ps.alignment, Alignment::Left);
1360 assert_eq!(ps.margin_left, HwpUnit::ZERO);
1361 assert_eq!(ps.indent, HwpUnit::ZERO);
1362 assert_eq!(ps.line_spacing, 160);
1363 assert_eq!(ps.line_spacing_type, LineSpacingType::Percentage);
1364 }
1365
1366 #[test]
1369 fn parse_hex_color_valid() {
1370 let c = parse_hex_color("#FF0000");
1371 assert_eq!(c.red(), 255);
1372 assert_eq!(c.green(), 0);
1373 assert_eq!(c.blue(), 0);
1374 }
1375
1376 #[test]
1377 fn parse_hex_color_lowercase() {
1378 let c = parse_hex_color("#00ff00");
1379 assert_eq!(c.green(), 255);
1380 }
1381
1382 #[test]
1383 fn parse_hex_color_no_hash() {
1384 let c = parse_hex_color("0000FF");
1385 assert_eq!(c.blue(), 255);
1386 }
1387
1388 #[test]
1389 fn parse_hex_color_none_returns_black() {
1390 assert_eq!(parse_hex_color("none"), Color::BLACK);
1391 assert_eq!(parse_hex_color("NONE"), Color::BLACK);
1392 }
1393
1394 #[test]
1395 fn parse_hex_color_empty_returns_black() {
1396 assert_eq!(parse_hex_color(""), Color::BLACK);
1397 }
1398
1399 #[test]
1400 fn parse_hex_color_invalid_returns_black() {
1401 assert_eq!(parse_hex_color("#GGHHII"), Color::BLACK);
1402 assert_eq!(parse_hex_color("#FFF"), Color::BLACK); assert_eq!(parse_hex_color("garbage"), Color::BLACK);
1404 }
1405
1406 #[test]
1407 fn parse_hex_color_white() {
1408 let c = parse_hex_color("#FFFFFF");
1409 assert_eq!(c, Color::WHITE);
1410 }
1411
1412 #[test]
1415 fn parse_alignment_standard() {
1416 assert_eq!(parse_alignment("LEFT"), Alignment::Left);
1417 assert_eq!(parse_alignment("CENTER"), Alignment::Center);
1418 assert_eq!(parse_alignment("RIGHT"), Alignment::Right);
1419 assert_eq!(parse_alignment("JUSTIFY"), Alignment::Justify);
1420 }
1421
1422 #[test]
1423 fn parse_alignment_both_maps_to_justify() {
1424 assert_eq!(parse_alignment("BOTH"), Alignment::Justify);
1426 }
1427
1428 #[test]
1429 fn parse_alignment_case_insensitive() {
1430 assert_eq!(parse_alignment("center"), Alignment::Center);
1431 assert_eq!(parse_alignment("Right"), Alignment::Right);
1432 }
1433
1434 #[test]
1435 fn parse_alignment_distribute() {
1436 assert_eq!(parse_alignment("DISTRIBUTE"), Alignment::Distribute);
1437 assert_eq!(parse_alignment("distribute"), Alignment::Distribute);
1438 assert_eq!(parse_alignment("DISTRIBUTE_FLUSH"), Alignment::DistributeFlush);
1439 assert_eq!(parse_alignment("distribute_flush"), Alignment::DistributeFlush);
1440 }
1441
1442 #[test]
1443 fn parse_alignment_unknown_defaults_left() {
1444 assert_eq!(parse_alignment("DISTRIBUTED"), Alignment::Left);
1445 assert_eq!(parse_alignment(""), Alignment::Left);
1446 }
1447
1448 #[test]
1451 fn push_and_get_style() {
1452 let mut store = HwpxStyleStore::new();
1453 let style = HwpxStyle {
1454 id: 0,
1455 style_type: "PARA".into(),
1456 name: "바탕글".into(),
1457 eng_name: "Normal".into(),
1458 para_pr_id_ref: 0,
1459 char_pr_id_ref: 0,
1460 next_style_id_ref: 0,
1461 lang_id: 1042,
1462 };
1463 store.push_style(style);
1464 assert_eq!(store.style_count(), 1);
1465 let s = store.style(0).unwrap();
1466 assert_eq!(s.name, "바탕글");
1467 assert_eq!(s.eng_name, "Normal");
1468 assert_eq!(s.style_type, "PARA");
1469 }
1470
1471 #[test]
1472 fn style_index_out_of_bounds() {
1473 let store = HwpxStyleStore::new();
1474 let err = store.style(0).unwrap_err();
1475 match err {
1476 HwpxError::IndexOutOfBounds { kind, index, max } => {
1477 assert_eq!(kind, "style");
1478 assert_eq!(index, 0);
1479 assert_eq!(max, 0);
1480 }
1481 _ => panic!("expected IndexOutOfBounds"),
1482 }
1483 }
1484
1485 #[test]
1486 fn iter_styles_yields_all() {
1487 let mut store = HwpxStyleStore::new();
1488 store.push_style(HwpxStyle {
1489 id: 0,
1490 style_type: "PARA".into(),
1491 name: "바탕글".into(),
1492 eng_name: "Normal".into(),
1493 para_pr_id_ref: 0,
1494 char_pr_id_ref: 0,
1495 next_style_id_ref: 0,
1496 lang_id: 1042,
1497 });
1498 store.push_style(HwpxStyle {
1499 id: 1,
1500 style_type: "CHAR".into(),
1501 name: "본문".into(),
1502 eng_name: "Body".into(),
1503 para_pr_id_ref: 1,
1504 char_pr_id_ref: 1,
1505 next_style_id_ref: 1,
1506 lang_id: 1042,
1507 });
1508 let names: Vec<&str> = store.iter_styles().map(|s| s.name.as_str()).collect();
1509 assert_eq!(names, vec!["바탕글", "본문"]);
1510 }
1511
1512 #[test]
1515 fn from_registry_empty_produces_empty_store() {
1516 let registry: StyleRegistry = serde_json::from_str(
1517 r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
1518 )
1519 .unwrap();
1520 let store = HwpxStyleStore::from_registry(®istry);
1521
1522 assert_eq!(store.font_count(), 7);
1526 assert_eq!(store.char_shape_count(), 7); assert_eq!(store.para_shape_count(), 20); assert_eq!(store.style_count(), 22);
1529 }
1530
1531 #[test]
1532 fn from_registry_preserves_counts() {
1533 let template = builtin_default().unwrap();
1534 let registry = StyleRegistry::from_template(&template).unwrap();
1535 let store = HwpxStyleStore::from_registry(®istry);
1536
1537 assert_eq!(store.font_count(), registry.font_count() * 7);
1539 assert_eq!(store.char_shape_count(), 7 + registry.char_shape_count());
1541 assert_eq!(store.para_shape_count(), 20 + registry.para_shape_count());
1542 assert_eq!(store.style_count(), registry.style_count() + 22);
1544 }
1545
1546 #[test]
1547 fn from_registry_font_face_names_match() {
1548 let template = builtin_default().unwrap();
1549 let registry = StyleRegistry::from_template(&template).unwrap();
1550 let store = HwpxStyleStore::from_registry(®istry);
1551
1552 let font_count = registry.font_count();
1553 let langs = ["HANGUL", "LATIN", "HANJA", "JAPANESE", "OTHER", "SYMBOL", "USER"];
1554 for (lang_idx, &lang) in langs.iter().enumerate() {
1556 for (font_idx, font_id) in registry.fonts.iter().enumerate() {
1557 let store_idx = lang_idx * font_count + font_idx;
1558 let hwpx_font = store.font(FontIndex::new(store_idx)).unwrap();
1559 assert_eq!(hwpx_font.face_name, font_id.as_str());
1560 assert_eq!(hwpx_font.lang, lang);
1561 }
1562 }
1563 }
1564
1565 #[test]
1566 fn from_registry_char_shape_properties() {
1567 let template = builtin_default().unwrap();
1568 let registry = StyleRegistry::from_template(&template).unwrap();
1569 let store = HwpxStyleStore::from_registry(®istry);
1570
1571 for (i, bp_cs) in registry.char_shapes.iter().enumerate() {
1573 let hwpx_cs = store.char_shape(CharShapeIndex::new(7 + i)).unwrap();
1574 assert_eq!(hwpx_cs.height, bp_cs.size);
1575 assert_eq!(hwpx_cs.text_color, bp_cs.color);
1576 assert_eq!(hwpx_cs.shade_color, bp_cs.shade_color);
1577 assert_eq!(hwpx_cs.bold, bp_cs.bold);
1578 assert_eq!(hwpx_cs.italic, bp_cs.italic);
1579 assert_eq!(hwpx_cs.underline_type, bp_cs.underline_type);
1580 assert_eq!(hwpx_cs.underline_color, bp_cs.underline_color);
1581 assert_eq!(hwpx_cs.strikeout_shape, bp_cs.strikeout_shape);
1582 assert_eq!(hwpx_cs.strikeout_color, bp_cs.strikeout_color);
1583 assert_eq!(hwpx_cs.vertical_position, bp_cs.vertical_position);
1584 assert_eq!(hwpx_cs.outline_type, bp_cs.outline);
1585 assert_eq!(hwpx_cs.shadow_type, bp_cs.shadow);
1586 assert_eq!(hwpx_cs.emboss_type, bp_cs.emboss);
1587 assert_eq!(hwpx_cs.engrave_type, bp_cs.engrave);
1588 }
1589 }
1590
1591 #[test]
1592 fn from_registry_para_shape_properties() {
1593 let template = builtin_default().unwrap();
1594 let registry = StyleRegistry::from_template(&template).unwrap();
1595 let store = HwpxStyleStore::from_registry(®istry);
1596
1597 for (i, bp_ps) in registry.para_shapes.iter().enumerate() {
1599 let hwpx_ps = store.para_shape(ParaShapeIndex::new(20 + i)).unwrap();
1600 assert_eq!(hwpx_ps.alignment, bp_ps.alignment);
1601 assert_eq!(hwpx_ps.margin_left, bp_ps.indent_left);
1602 assert_eq!(hwpx_ps.margin_right, bp_ps.indent_right);
1603 assert_eq!(hwpx_ps.indent, bp_ps.indent_first_line);
1604 assert_eq!(hwpx_ps.spacing_before, bp_ps.space_before);
1605 assert_eq!(hwpx_ps.spacing_after, bp_ps.space_after);
1606 assert_eq!(hwpx_ps.line_spacing, bp_ps.line_spacing_value.round() as i32);
1607 }
1608 }
1609
1610 #[test]
1611 fn from_registry_style_entries_reference_valid_indices() {
1612 let template = builtin_default().unwrap();
1613 let registry = StyleRegistry::from_template(&template).unwrap();
1614 let store = HwpxStyleStore::from_registry(®istry);
1615
1616 for i in 0..store.style_count() {
1617 let style = store.style(i).unwrap();
1618 assert!(
1620 style.style_type == "PARA" || style.style_type == "CHAR",
1621 "unexpected style_type '{}' for style '{}'",
1622 style.style_type,
1623 style.name
1624 );
1625 assert!(
1626 (style.char_pr_id_ref as usize) < store.char_shape_count(),
1627 "char_pr_id_ref {} out of bounds for style '{}'",
1628 style.char_pr_id_ref,
1629 style.name
1630 );
1631 assert!(
1632 (style.para_pr_id_ref as usize) < store.para_shape_count(),
1633 "para_pr_id_ref {} out of bounds for style '{}'",
1634 style.para_pr_id_ref,
1635 style.name
1636 );
1637 }
1638 }
1639
1640 #[test]
1643 fn default_style_set_classic_count() {
1644 assert_eq!(HancomStyleSet::Classic.count(), 18);
1645 }
1646
1647 #[test]
1648 fn default_style_set_modern_count() {
1649 assert_eq!(HancomStyleSet::Modern.count(), 22);
1650 }
1651
1652 #[test]
1653 fn default_style_set_latest_count() {
1654 assert_eq!(HancomStyleSet::Latest.count(), 23);
1655 }
1656
1657 #[test]
1658 fn default_style_set_modern_is_default() {
1659 assert_eq!(HancomStyleSet::default(), HancomStyleSet::Modern);
1660 }
1661
1662 #[test]
1665 fn with_default_fonts_creates_seven_fonts() {
1666 let store = HwpxStyleStore::with_default_fonts("함초롬돋움");
1667 assert_eq!(store.font_count(), 7);
1668 }
1669
1670 #[test]
1671 fn with_default_fonts_all_names_match() {
1672 let font_name = "나눔고딕";
1673 let store = HwpxStyleStore::with_default_fonts(font_name);
1674 for font in store.iter_fonts() {
1675 assert_eq!(font.face_name, font_name);
1676 }
1677 }
1678
1679 #[test]
1680 fn with_default_fonts_lang_groups_correct() {
1681 let store = HwpxStyleStore::with_default_fonts("함초롬바탕");
1682 let langs: Vec<&str> = store.iter_fonts().map(|f| f.lang.as_str()).collect();
1683 assert_eq!(langs, vec!["HANGUL", "LATIN", "HANJA", "JAPANESE", "OTHER", "SYMBOL", "USER"]);
1684 }
1685
1686 #[test]
1687 fn from_registry_with_classic_style_set() {
1688 let registry: StyleRegistry = serde_json::from_str(
1689 r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
1690 )
1691 .unwrap();
1692 let store = HwpxStyleStore::from_registry_with(®istry, HancomStyleSet::Classic);
1693 assert_eq!(store.style_set(), HancomStyleSet::Classic);
1694 assert_eq!(store.style_count(), 18);
1696 assert_eq!(store.style(9).unwrap().name, "쪽 번호");
1698 }
1699
1700 #[test]
1701 fn modern_styles_match_golden_fixture() {
1702 let styles = HancomStyleSet::Modern.default_styles();
1704 assert_eq!(styles[9].name, "개요 8");
1706 assert_eq!(styles[10].name, "개요 9");
1707 assert_eq!(styles[11].name, "개요 10");
1708 assert_eq!(styles[12].name, "쪽 번호");
1710 assert_eq!(styles[12].style_type, "CHAR");
1711 assert_eq!(styles[21].name, "캡션");
1713 assert_eq!(styles[21].style_type, "PARA");
1714 }
1715
1716 #[test]
1719 fn default_border_fills_count() {
1720 use hwpforge_blueprint::{builtins::builtin_default, registry::StyleRegistry};
1721 let template = builtin_default().unwrap();
1722 let registry = StyleRegistry::from_template(&template).unwrap();
1723 let store = HwpxStyleStore::from_registry(®istry);
1724 assert_eq!(store.border_fill_count(), 3, "from_registry produces exactly 3 default fills");
1725 }
1726
1727 #[test]
1728 fn default_border_fill_page() {
1729 let bf = HwpxBorderFill::default_page_border();
1731 assert_eq!(bf.id, 1);
1732 assert!(!bf.three_d);
1733 assert!(!bf.shadow);
1734 assert_eq!(bf.center_line, "NONE");
1735 assert_eq!(bf.left.line_type, "NONE");
1736 assert_eq!(bf.right.line_type, "NONE");
1737 assert_eq!(bf.top.line_type, "NONE");
1738 assert_eq!(bf.bottom.line_type, "NONE");
1739 assert_eq!(bf.diagonal.line_type, "SOLID");
1740 assert!(bf.fill.is_none());
1741 }
1742
1743 #[test]
1744 fn default_border_fill_char() {
1745 let bf = HwpxBorderFill::default_char_background();
1747 assert_eq!(bf.id, 2);
1748 assert!(bf.fill.is_some(), "char background must have a fill brush");
1749 match bf.fill.as_ref().unwrap() {
1750 HwpxFill::WinBrush { face_color, hatch_color, alpha } => {
1751 assert_eq!(face_color, "none");
1752 assert_eq!(hatch_color, "#FF000000");
1753 assert_eq!(alpha, "0");
1754 }
1755 }
1756 }
1757
1758 #[test]
1759 fn default_border_fill_table() {
1760 let bf = HwpxBorderFill::default_table_border();
1762 assert_eq!(bf.id, 3);
1763 assert_eq!(bf.left.line_type, "SOLID");
1764 assert_eq!(bf.left.width, "0.12 mm");
1765 assert_eq!(bf.right.line_type, "SOLID");
1766 assert_eq!(bf.top.line_type, "SOLID");
1767 assert_eq!(bf.bottom.line_type, "SOLID");
1768 assert_eq!(bf.diagonal.line_type, "SOLID");
1769 assert_eq!(bf.diagonal.width, "0.1 mm");
1770 assert!(bf.fill.is_none());
1771 }
1772
1773 #[test]
1774 fn push_user_border_fill() {
1775 let mut store = HwpxStyleStore::new();
1776 let bf = HwpxBorderFill {
1777 id: 4,
1778 three_d: false,
1779 shadow: false,
1780 center_line: "NONE".into(),
1781 left: HwpxBorderLine {
1782 line_type: "DASH".into(),
1783 width: "0.2 mm".into(),
1784 color: "#FF0000".into(),
1785 },
1786 right: HwpxBorderLine::default(),
1787 top: HwpxBorderLine::default(),
1788 bottom: HwpxBorderLine::default(),
1789 diagonal: HwpxBorderLine::default(),
1790 slash_type: "NONE".into(),
1791 back_slash_type: "NONE".into(),
1792 fill: None,
1793 };
1794 let returned_id = store.push_border_fill(bf);
1795 assert_eq!(returned_id, 4);
1796 assert_eq!(store.border_fill_count(), 1);
1797 let fetched = store.border_fill(4).unwrap();
1798 assert_eq!(fetched.left.line_type, "DASH");
1799 assert_eq!(fetched.left.width, "0.2 mm");
1800 }
1801
1802 #[test]
1803 fn border_fill_not_found_returns_error() {
1804 let store = HwpxStyleStore::new();
1805 assert!(store.border_fill(1).is_err());
1806 }
1807
1808 #[test]
1809 fn from_registry_border_fills_have_correct_ids() {
1810 use hwpforge_blueprint::{builtins::builtin_default, registry::StyleRegistry};
1811 let template = builtin_default().unwrap();
1812 let registry = StyleRegistry::from_template(&template).unwrap();
1813 let store = HwpxStyleStore::from_registry(®istry);
1814 assert_eq!(store.border_fill(1).unwrap().id, 1);
1816 assert_eq!(store.border_fill(2).unwrap().id, 2);
1817 assert_eq!(store.border_fill(3).unwrap().id, 3);
1818 }
1819
1820 #[test]
1823 fn from_registry_injects_7_default_char_shapes() {
1824 let registry: StyleRegistry = serde_json::from_str(
1825 r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
1826 )
1827 .unwrap();
1828 let store = HwpxStyleStore::from_registry(®istry);
1829 assert_eq!(store.char_shape_count(), 7, "must have exactly 7 default charPr groups");
1830 }
1831
1832 #[test]
1833 fn from_registry_injects_20_default_para_shapes() {
1834 let registry: StyleRegistry = serde_json::from_str(
1835 r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
1836 )
1837 .unwrap();
1838 let store = HwpxStyleStore::from_registry(®istry);
1839 assert_eq!(store.para_shape_count(), 20, "must have exactly 20 default paraPr groups");
1840 }
1841
1842 #[test]
1843 fn default_char_shape_0_is_batang_10pt_black() {
1844 let registry: StyleRegistry = serde_json::from_str(
1846 r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
1847 )
1848 .unwrap();
1849 let store = HwpxStyleStore::from_registry(®istry);
1850 let cs = store.char_shape(CharShapeIndex::new(0)).unwrap();
1851 assert_eq!(cs.height.as_i32(), 1000); assert_eq!(cs.text_color, Color::BLACK);
1853 assert!(!cs.bold);
1854 assert!(!cs.italic);
1855 }
1856
1857 #[test]
1858 fn default_char_shape_5_is_toc_heading() {
1859 let registry: StyleRegistry = serde_json::from_str(
1861 r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
1862 )
1863 .unwrap();
1864 let store = HwpxStyleStore::from_registry(®istry);
1865 let cs = store.char_shape(CharShapeIndex::new(5)).unwrap();
1866 assert_eq!(cs.height.as_i32(), 1600); assert_eq!(cs.text_color, Color::from_rgb(0x2E, 0x74, 0xB5));
1868 }
1869
1870 #[test]
1871 fn from_registry_user_shapes_offset() {
1872 let template = builtin_default().unwrap();
1874 let registry = StyleRegistry::from_template(&template).unwrap();
1875 let store = HwpxStyleStore::from_registry(®istry);
1876 assert!(store.char_shape(CharShapeIndex::new(7)).is_ok());
1878 assert!(store.para_shape(ParaShapeIndex::new(20)).is_ok());
1880 }
1881
1882 #[test]
1883 fn from_registry_default_style_refs_match_groups() {
1884 let registry: StyleRegistry = serde_json::from_str(
1886 r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
1887 )
1888 .unwrap();
1889 let store = HwpxStyleStore::from_registry(®istry);
1890 let defaults = HancomStyleSet::Modern.default_styles();
1891 for (idx, entry) in defaults.iter().enumerate() {
1892 let style = store.style(idx).unwrap();
1893 assert_eq!(
1894 style.char_pr_id_ref, entry.char_pr_group as u32,
1895 "charPr ref mismatch for style '{}'",
1896 entry.name
1897 );
1898 assert_eq!(
1899 style.para_pr_id_ref, entry.para_pr_group as u32,
1900 "paraPr ref mismatch for style '{}'",
1901 entry.name
1902 );
1903 }
1904 }
1905
1906 #[test]
1907 fn from_registry_user_style_refs_are_offset_adjusted() {
1908 let template = builtin_default().unwrap();
1910 let registry = StyleRegistry::from_template(&template).unwrap();
1911 let store = HwpxStyleStore::from_registry(®istry);
1912 let defaults_len = HancomStyleSet::Modern.count();
1913 for (i, (_, entry)) in registry.style_entries.iter().enumerate() {
1914 let style = store.style(defaults_len + i).unwrap();
1915 assert_eq!(
1916 style.char_pr_id_ref,
1917 (entry.char_shape_id.get() + 7) as u32,
1918 "user charPr ref not offset-adjusted for style index {i}"
1919 );
1920 assert_eq!(
1921 style.para_pr_id_ref,
1922 (entry.para_shape_id.get() + 20) as u32,
1923 "user paraPr ref not offset-adjusted for style index {i}"
1924 );
1925 }
1926 }
1927
1928 #[test]
1929 fn default_para_shape_0_is_batanggeul() {
1930 let registry: StyleRegistry = serde_json::from_str(
1932 r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
1933 )
1934 .unwrap();
1935 let store = HwpxStyleStore::from_registry(®istry);
1936 let ps = store.para_shape(ParaShapeIndex::new(0)).unwrap();
1937 assert_eq!(ps.alignment, Alignment::Justify);
1938 assert_eq!(ps.margin_left.as_i32(), 0);
1939 assert_eq!(ps.line_spacing, 160);
1940 }
1941
1942 #[test]
1943 fn default_para_shape_2_is_outline1() {
1944 let registry: StyleRegistry = serde_json::from_str(
1946 r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
1947 )
1948 .unwrap();
1949 let store = HwpxStyleStore::from_registry(®istry);
1950 let ps = store.para_shape(ParaShapeIndex::new(2)).unwrap();
1951 assert_eq!(ps.alignment, Alignment::Justify);
1952 assert_eq!(ps.margin_left.as_i32(), 1000);
1953 assert_eq!(ps.line_spacing, 160);
1954 }
1955}