1use hwpforge_foundation::{
14 Alignment, BorderFillIndex, BreakType, Color, EmbossType, EmphasisType, EngraveType,
15 HeadingType, HwpUnit, LineSpacingType, OutlineType, ShadowType, StrikeoutShape, UnderlineType,
16 VerticalPosition,
17};
18use schemars::JsonSchema;
19use serde::{Deserialize, Serialize};
20
21use crate::error::{BlueprintError, BlueprintResult};
22use crate::serde_helpers::{
23 de_color, de_color_opt, de_dim, de_dim_opt, de_pct_opt, ser_color, ser_color_opt, ser_dim,
24 ser_dim_opt, ser_pct_opt,
25};
26
27#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
33pub struct Spacing {
34 #[serde(
36 default,
37 serialize_with = "ser_dim_opt",
38 deserialize_with = "de_dim_opt",
39 skip_serializing_if = "Option::is_none"
40 )]
41 pub before: Option<HwpUnit>,
42 #[serde(
44 default,
45 serialize_with = "ser_dim_opt",
46 deserialize_with = "de_dim_opt",
47 skip_serializing_if = "Option::is_none"
48 )]
49 pub after: Option<HwpUnit>,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
54pub struct Indent {
55 #[serde(
57 default,
58 serialize_with = "ser_dim_opt",
59 deserialize_with = "de_dim_opt",
60 skip_serializing_if = "Option::is_none"
61 )]
62 pub left: Option<HwpUnit>,
63 #[serde(
65 default,
66 serialize_with = "ser_dim_opt",
67 deserialize_with = "de_dim_opt",
68 skip_serializing_if = "Option::is_none"
69 )]
70 pub right: Option<HwpUnit>,
71 #[serde(
73 default,
74 serialize_with = "ser_dim_opt",
75 deserialize_with = "de_dim_opt",
76 skip_serializing_if = "Option::is_none"
77 )]
78 pub first_line: Option<HwpUnit>,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
83pub struct LineSpacing {
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub spacing_type: Option<LineSpacingType>,
87 #[serde(
89 default,
90 serialize_with = "ser_pct_opt",
91 deserialize_with = "de_pct_opt",
92 skip_serializing_if = "Option::is_none"
93 )]
94 pub value: Option<f64>,
95}
96
97#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
106pub struct PartialCharShape {
107 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub font: Option<String>,
110 #[serde(
112 default,
113 serialize_with = "ser_dim_opt",
114 deserialize_with = "de_dim_opt",
115 skip_serializing_if = "Option::is_none"
116 )]
117 pub size: Option<HwpUnit>,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
120 pub bold: Option<bool>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub italic: Option<bool>,
124 #[serde(
126 default,
127 serialize_with = "ser_color_opt",
128 deserialize_with = "de_color_opt",
129 skip_serializing_if = "Option::is_none"
130 )]
131 pub color: Option<Color>,
132
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub underline_type: Option<UnderlineType>,
136 #[serde(
138 default,
139 serialize_with = "ser_color_opt",
140 deserialize_with = "de_color_opt",
141 skip_serializing_if = "Option::is_none"
142 )]
143 pub underline_color: Option<Color>,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub strikeout_shape: Option<StrikeoutShape>,
147 #[serde(
149 default,
150 serialize_with = "ser_color_opt",
151 deserialize_with = "de_color_opt",
152 skip_serializing_if = "Option::is_none"
153 )]
154 pub strikeout_color: Option<Color>,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
157 pub outline: Option<OutlineType>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub shadow: Option<ShadowType>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub emboss: Option<EmbossType>,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub engrave: Option<EngraveType>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub vertical_position: Option<VerticalPosition>,
171 #[serde(
173 default,
174 serialize_with = "ser_color_opt",
175 deserialize_with = "de_color_opt",
176 skip_serializing_if = "Option::is_none"
177 )]
178 pub shade_color: Option<Color>,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub emphasis: Option<EmphasisType>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub ratio: Option<i32>,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub spacing: Option<i32>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub rel_sz: Option<i32>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
197 pub offset: Option<i32>,
198 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub use_kerning: Option<bool>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
203 pub use_font_space: Option<bool>,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub char_border_fill_id: Option<u32>,
210}
211
212impl PartialCharShape {
213 pub fn merge(&mut self, other: &PartialCharShape) {
216 if other.font.is_some() {
217 self.font.clone_from(&other.font);
218 }
219 if other.size.is_some() {
220 self.size = other.size;
221 }
222 if other.bold.is_some() {
223 self.bold = other.bold;
224 }
225 if other.italic.is_some() {
226 self.italic = other.italic;
227 }
228 if other.color.is_some() {
229 self.color = other.color;
230 }
231 if other.underline_type.is_some() {
232 self.underline_type = other.underline_type;
233 }
234 if other.underline_color.is_some() {
235 self.underline_color = other.underline_color;
236 }
237 if other.strikeout_shape.is_some() {
238 self.strikeout_shape = other.strikeout_shape;
239 }
240 if other.strikeout_color.is_some() {
241 self.strikeout_color = other.strikeout_color;
242 }
243 if other.outline.is_some() {
244 self.outline = other.outline;
245 }
246 if other.shadow.is_some() {
247 self.shadow = other.shadow;
248 }
249 if other.emboss.is_some() {
250 self.emboss = other.emboss;
251 }
252 if other.engrave.is_some() {
253 self.engrave = other.engrave;
254 }
255 if other.vertical_position.is_some() {
256 self.vertical_position = other.vertical_position;
257 }
258 if other.shade_color.is_some() {
259 self.shade_color = other.shade_color;
260 }
261 if other.emphasis.is_some() {
262 self.emphasis = other.emphasis;
263 }
264 if other.ratio.is_some() {
265 self.ratio = other.ratio;
266 }
267 if other.spacing.is_some() {
268 self.spacing = other.spacing;
269 }
270 if other.rel_sz.is_some() {
271 self.rel_sz = other.rel_sz;
272 }
273 if other.offset.is_some() {
274 self.offset = other.offset;
275 }
276 if other.use_kerning.is_some() {
277 self.use_kerning = other.use_kerning;
278 }
279 if other.use_font_space.is_some() {
280 self.use_font_space = other.use_font_space;
281 }
282 if other.char_border_fill_id.is_some() {
283 self.char_border_fill_id = other.char_border_fill_id;
284 }
285 }
286
287 pub fn resolve(&self, style_name: &str) -> BlueprintResult<CharShape> {
291 Ok(CharShape {
292 font: self.font.clone().ok_or_else(|| BlueprintError::StyleResolution {
293 style_name: style_name.to_string(),
294 field: "font".to_string(),
295 })?,
296 size: self.size.ok_or_else(|| BlueprintError::StyleResolution {
297 style_name: style_name.to_string(),
298 field: "size".to_string(),
299 })?,
300 bold: self.bold.unwrap_or(false),
301 italic: self.italic.unwrap_or(false),
302 color: self.color.unwrap_or(Color::BLACK),
303 underline_type: self.underline_type.unwrap_or(UnderlineType::None),
304 underline_color: self.underline_color,
305 strikeout_shape: self.strikeout_shape.unwrap_or(StrikeoutShape::None),
306 strikeout_color: self.strikeout_color,
307 outline: self.outline.unwrap_or(OutlineType::None),
308 shadow: self.shadow.unwrap_or(ShadowType::None),
309 emboss: self.emboss.unwrap_or(EmbossType::None),
310 engrave: self.engrave.unwrap_or(EngraveType::None),
311 vertical_position: self.vertical_position.unwrap_or(VerticalPosition::Normal),
312 shade_color: self.shade_color,
313 emphasis: self.emphasis.unwrap_or(EmphasisType::None),
314 ratio: self.ratio.unwrap_or(100),
315 spacing: self.spacing.unwrap_or(0),
316 rel_sz: self.rel_sz.unwrap_or(100),
317 offset: self.offset.unwrap_or(0),
318 use_kerning: self.use_kerning.unwrap_or(false),
319 use_font_space: self.use_font_space.unwrap_or(false),
320 char_border_fill_id: self.char_border_fill_id,
321 })
322 }
323}
324
325#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
327pub struct PartialParaShape {
328 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub alignment: Option<Alignment>,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub line_spacing: Option<LineSpacing>,
334 #[serde(default, skip_serializing_if = "Option::is_none")]
336 pub spacing: Option<Spacing>,
337 #[serde(default, skip_serializing_if = "Option::is_none")]
339 pub indent: Option<Indent>,
340
341 #[serde(default, skip_serializing_if = "Option::is_none")]
344 pub break_type: Option<BreakType>,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub keep_with_next: Option<bool>,
348 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub keep_lines_together: Option<bool>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub widow_orphan: Option<bool>,
354 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub border_fill_id: Option<BorderFillIndex>,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub heading_type: Option<HeadingType>,
360}
361
362impl Spacing {
363 pub fn merge(&mut self, other: &Spacing) {
365 if other.before.is_some() {
366 self.before = other.before;
367 }
368 if other.after.is_some() {
369 self.after = other.after;
370 }
371 }
372}
373
374impl Indent {
375 pub fn merge(&mut self, other: &Indent) {
377 if other.left.is_some() {
378 self.left = other.left;
379 }
380 if other.right.is_some() {
381 self.right = other.right;
382 }
383 if other.first_line.is_some() {
384 self.first_line = other.first_line;
385 }
386 }
387}
388
389impl LineSpacing {
390 pub fn merge(&mut self, other: &LineSpacing) {
392 if other.spacing_type.is_some() {
393 self.spacing_type = other.spacing_type;
394 }
395 if other.value.is_some() {
396 self.value = other.value;
397 }
398 }
399}
400
401impl PartialParaShape {
402 pub fn merge(&mut self, other: &PartialParaShape) {
408 if other.alignment.is_some() {
409 self.alignment = other.alignment;
410 }
411 match (&mut self.line_spacing, &other.line_spacing) {
413 (Some(base), Some(child)) => base.merge(child),
414 (None, Some(child)) => self.line_spacing = Some(*child),
415 _ => {}
416 }
417 match (&mut self.spacing, &other.spacing) {
418 (Some(base), Some(child)) => base.merge(child),
419 (None, Some(child)) => self.spacing = Some(*child),
420 _ => {}
421 }
422 match (&mut self.indent, &other.indent) {
423 (Some(base), Some(child)) => base.merge(child),
424 (None, Some(child)) => self.indent = Some(*child),
425 _ => {}
426 }
427 if other.break_type.is_some() {
428 self.break_type = other.break_type;
429 }
430 if other.keep_with_next.is_some() {
431 self.keep_with_next = other.keep_with_next;
432 }
433 if other.keep_lines_together.is_some() {
434 self.keep_lines_together = other.keep_lines_together;
435 }
436 if other.widow_orphan.is_some() {
437 self.widow_orphan = other.widow_orphan;
438 }
439 if other.border_fill_id.is_some() {
440 self.border_fill_id = other.border_fill_id;
441 }
442 if other.heading_type.is_some() {
443 self.heading_type = other.heading_type;
444 }
445 }
446
447 pub fn resolve(&self) -> ParaShape {
449 ParaShape {
450 alignment: self.alignment.unwrap_or(Alignment::Left),
451 line_spacing_type: self
452 .line_spacing
453 .and_then(|ls| ls.spacing_type)
454 .unwrap_or(LineSpacingType::Percentage),
455 line_spacing_value: self.line_spacing.and_then(|ls| ls.value).unwrap_or(160.0),
456 space_before: self.spacing.and_then(|s| s.before).unwrap_or(HwpUnit::ZERO),
457 space_after: self.spacing.and_then(|s| s.after).unwrap_or(HwpUnit::ZERO),
458 indent_left: self.indent.and_then(|i| i.left).unwrap_or(HwpUnit::ZERO),
459 indent_right: self.indent.and_then(|i| i.right).unwrap_or(HwpUnit::ZERO),
460 indent_first_line: self.indent.and_then(|i| i.first_line).unwrap_or(HwpUnit::ZERO),
461 break_type: self.break_type.unwrap_or(BreakType::None),
462 keep_with_next: self.keep_with_next.unwrap_or(false),
463 keep_lines_together: self.keep_lines_together.unwrap_or(false),
464 widow_orphan: self.widow_orphan.unwrap_or(true), border_fill_id: self.border_fill_id,
466 heading_type: self.heading_type.unwrap_or(HeadingType::None),
467 }
468 }
469}
470
471#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
473pub struct PartialStyle {
474 #[serde(default, skip_serializing_if = "Option::is_none")]
476 pub char_shape: Option<PartialCharShape>,
477 #[serde(default, skip_serializing_if = "Option::is_none")]
479 pub para_shape: Option<PartialParaShape>,
480}
481
482impl PartialStyle {
483 pub fn merge(&mut self, other: &PartialStyle) {
485 match (&mut self.char_shape, &other.char_shape) {
486 (Some(base), Some(child)) => base.merge(child),
487 (None, Some(child)) => self.char_shape = Some(child.clone()),
488 _ => {}
489 }
490 match (&mut self.para_shape, &other.para_shape) {
491 (Some(base), Some(child)) => base.merge(child),
492 (None, Some(child)) => self.para_shape = Some(child.clone()),
493 _ => {}
494 }
495 }
496}
497
498#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
504pub struct CharShape {
505 pub font: String,
507 #[serde(serialize_with = "ser_dim", deserialize_with = "de_dim")]
509 pub size: HwpUnit,
510 pub bold: bool,
512 pub italic: bool,
514 #[serde(serialize_with = "ser_color", deserialize_with = "de_color")]
516 pub color: Color,
517 pub underline_type: UnderlineType,
519 #[serde(
521 default,
522 serialize_with = "ser_color_opt",
523 deserialize_with = "de_color_opt",
524 skip_serializing_if = "Option::is_none"
525 )]
526 pub underline_color: Option<Color>,
527 pub strikeout_shape: StrikeoutShape,
529 #[serde(
531 default,
532 serialize_with = "ser_color_opt",
533 deserialize_with = "de_color_opt",
534 skip_serializing_if = "Option::is_none"
535 )]
536 pub strikeout_color: Option<Color>,
537 pub outline: OutlineType,
539 pub shadow: ShadowType,
541 pub emboss: EmbossType,
543 pub engrave: EngraveType,
545 pub vertical_position: VerticalPosition,
547 #[serde(
549 default,
550 serialize_with = "ser_color_opt",
551 deserialize_with = "de_color_opt",
552 skip_serializing_if = "Option::is_none"
553 )]
554 pub shade_color: Option<Color>,
555 pub emphasis: EmphasisType,
557 pub ratio: i32,
559 pub spacing: i32,
561 pub rel_sz: i32,
563 pub offset: i32,
565 pub use_kerning: bool,
567 pub use_font_space: bool,
569 #[serde(default, skip_serializing_if = "Option::is_none")]
575 pub char_border_fill_id: Option<u32>,
576}
577
578#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
580pub struct ParaShape {
581 pub alignment: Alignment,
583 pub line_spacing_type: LineSpacingType,
585 pub line_spacing_value: f64,
587 #[serde(serialize_with = "ser_dim", deserialize_with = "de_dim")]
589 pub space_before: HwpUnit,
590 #[serde(serialize_with = "ser_dim", deserialize_with = "de_dim")]
592 pub space_after: HwpUnit,
593 #[serde(serialize_with = "ser_dim", deserialize_with = "de_dim")]
595 pub indent_left: HwpUnit,
596 #[serde(serialize_with = "ser_dim", deserialize_with = "de_dim")]
598 pub indent_right: HwpUnit,
599 #[serde(serialize_with = "ser_dim", deserialize_with = "de_dim")]
601 pub indent_first_line: HwpUnit,
602
603 pub break_type: BreakType,
606 pub keep_with_next: bool,
608 pub keep_lines_together: bool,
610 pub widow_orphan: bool,
612 #[serde(default, skip_serializing_if = "Option::is_none")]
614 pub border_fill_id: Option<BorderFillIndex>,
615 #[serde(default)]
617 pub heading_type: HeadingType,
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623 use pretty_assertions::assert_eq;
624
625 #[test]
626 fn partial_char_shape_default_is_all_none() {
627 let p = PartialCharShape::default();
628 assert!(p.font.is_none());
629 assert!(p.size.is_none());
630 assert!(p.bold.is_none());
631 assert!(p.italic.is_none());
632 assert!(p.color.is_none());
633 assert!(p.underline_type.is_none());
634 assert!(p.strikeout_shape.is_none());
635 assert!(p.vertical_position.is_none());
636 }
637
638 #[test]
639 fn partial_char_shape_merge_overrides() {
640 let mut base = PartialCharShape {
641 font: Some("Arial".into()),
642 size: Some(HwpUnit::from_pt(10.0).unwrap()),
643 bold: Some(false),
644 ..Default::default()
645 };
646 let child = PartialCharShape {
647 size: Some(HwpUnit::from_pt(16.0).unwrap()),
648 bold: Some(true),
649 ..Default::default()
650 };
651 base.merge(&child);
652 assert_eq!(base.font, Some("Arial".into()));
653 assert_eq!(base.size, Some(HwpUnit::from_pt(16.0).unwrap()));
654 assert_eq!(base.bold, Some(true));
655 }
656
657 #[test]
658 fn partial_char_shape_merge_none_does_not_override() {
659 let mut base = PartialCharShape { font: Some("Batang".into()), ..Default::default() };
660 let child = PartialCharShape::default();
661 base.merge(&child);
662 assert_eq!(base.font, Some("Batang".into()));
663 }
664
665 #[test]
666 fn partial_char_shape_resolve_success() {
667 let partial = PartialCharShape {
668 font: Some("한컴바탕".into()),
669 size: Some(HwpUnit::from_pt(10.0).unwrap()),
670 ..Default::default()
671 };
672 let resolved = partial.resolve("body").unwrap();
673 assert_eq!(resolved.font, "한컴바탕");
674 assert_eq!(resolved.size, HwpUnit::from_pt(10.0).unwrap());
675 assert!(!resolved.bold);
676 assert_eq!(resolved.color, Color::BLACK);
677 }
678
679 #[test]
680 fn partial_char_shape_resolve_missing_font() {
681 let partial =
682 PartialCharShape { size: Some(HwpUnit::from_pt(10.0).unwrap()), ..Default::default() };
683 let err = partial.resolve("heading1").unwrap_err();
684 assert!(err.to_string().contains("font"));
685 assert!(err.to_string().contains("heading1"));
686 }
687
688 #[test]
689 fn partial_char_shape_resolve_missing_size() {
690 let partial = PartialCharShape { font: Some("Arial".into()), ..Default::default() };
691 let err = partial.resolve("body").unwrap_err();
692 assert!(err.to_string().contains("size"));
693 }
694
695 #[test]
696 fn partial_para_shape_default_is_all_none() {
697 let p = PartialParaShape::default();
698 assert!(p.alignment.is_none());
699 assert!(p.line_spacing.is_none());
700 assert!(p.spacing.is_none());
701 assert!(p.indent.is_none());
702 }
703
704 #[test]
705 fn partial_para_shape_merge_overrides() {
706 let mut base = PartialParaShape {
707 alignment: Some(Alignment::Left),
708 line_spacing: Some(LineSpacing {
709 spacing_type: Some(LineSpacingType::Percentage),
710 value: Some(160.0),
711 }),
712 ..Default::default()
713 };
714 let child = PartialParaShape {
715 line_spacing: Some(LineSpacing { spacing_type: None, value: Some(170.0) }),
716 ..Default::default()
717 };
718 base.merge(&child);
719 assert_eq!(base.alignment, Some(Alignment::Left));
720 let ls = base.line_spacing.unwrap();
721 assert_eq!(ls.value, Some(170.0)); assert_eq!(ls.spacing_type, Some(LineSpacingType::Percentage)); }
724
725 #[test]
726 fn partial_para_shape_deep_merge_spacing() {
727 let mut base = PartialParaShape {
728 spacing: Some(Spacing { before: Some(HwpUnit::from_pt(6.0).unwrap()), after: None }),
729 ..Default::default()
730 };
731 let child = PartialParaShape {
732 spacing: Some(Spacing { before: None, after: Some(HwpUnit::from_pt(12.0).unwrap()) }),
733 ..Default::default()
734 };
735 base.merge(&child);
736 let sp = base.spacing.unwrap();
737 assert_eq!(sp.before, Some(HwpUnit::from_pt(6.0).unwrap())); assert_eq!(sp.after, Some(HwpUnit::from_pt(12.0).unwrap())); }
740
741 #[test]
742 fn partial_para_shape_deep_merge_indent() {
743 let mut base = PartialParaShape {
744 indent: Some(Indent {
745 left: Some(HwpUnit::from_pt(10.0).unwrap()),
746 right: None,
747 first_line: Some(HwpUnit::from_pt(5.0).unwrap()),
748 }),
749 ..Default::default()
750 };
751 let child = PartialParaShape {
752 indent: Some(Indent {
753 left: None,
754 right: Some(HwpUnit::from_pt(8.0).unwrap()),
755 first_line: None,
756 }),
757 ..Default::default()
758 };
759 base.merge(&child);
760 let indent = base.indent.unwrap();
761 assert_eq!(indent.left, Some(HwpUnit::from_pt(10.0).unwrap())); assert_eq!(indent.right, Some(HwpUnit::from_pt(8.0).unwrap())); assert_eq!(indent.first_line, Some(HwpUnit::from_pt(5.0).unwrap())); }
765
766 #[test]
767 fn partial_para_shape_resolve_defaults() {
768 let partial = PartialParaShape::default();
769 let resolved = partial.resolve();
770 assert_eq!(resolved.alignment, Alignment::Left);
771 assert_eq!(resolved.line_spacing_type, LineSpacingType::Percentage);
772 assert_eq!(resolved.line_spacing_value, 160.0);
773 assert_eq!(resolved.space_before, HwpUnit::ZERO);
774 assert_eq!(resolved.indent_left, HwpUnit::ZERO);
775 }
776
777 #[test]
778 fn partial_style_merge_both_present() {
779 let mut base = PartialStyle {
780 char_shape: Some(PartialCharShape {
781 font: Some("Arial".into()),
782 size: Some(HwpUnit::from_pt(10.0).unwrap()),
783 ..Default::default()
784 }),
785 para_shape: Some(PartialParaShape {
786 alignment: Some(Alignment::Left),
787 ..Default::default()
788 }),
789 };
790 let child = PartialStyle {
791 char_shape: Some(PartialCharShape {
792 size: Some(HwpUnit::from_pt(16.0).unwrap()),
793 bold: Some(true),
794 ..Default::default()
795 }),
796 para_shape: None,
797 };
798 base.merge(&child);
799 let cs = base.char_shape.unwrap();
800 assert_eq!(cs.font, Some("Arial".into()));
801 assert_eq!(cs.size, Some(HwpUnit::from_pt(16.0).unwrap()));
802 assert_eq!(cs.bold, Some(true));
803 }
804
805 #[test]
806 fn partial_style_merge_none_base() {
807 let mut base = PartialStyle::default();
808 let child = PartialStyle {
809 char_shape: Some(PartialCharShape { font: Some("Dotum".into()), ..Default::default() }),
810 para_shape: None,
811 };
812 base.merge(&child);
813 assert_eq!(base.char_shape.unwrap().font, Some("Dotum".into()));
814 }
815
816 #[test]
817 fn char_shape_serde_roundtrip() {
818 let original = CharShape {
819 font: "한컴바탕".into(),
820 size: HwpUnit::from_pt(16.0).unwrap(),
821 bold: true,
822 italic: false,
823 color: Color::from_rgb(0x00, 0x33, 0x66),
824 underline_type: UnderlineType::None,
825 underline_color: None,
826 strikeout_shape: StrikeoutShape::None,
827 strikeout_color: None,
828 outline: OutlineType::None,
829 shadow: ShadowType::None,
830 emboss: EmbossType::None,
831 engrave: EngraveType::None,
832 vertical_position: VerticalPosition::Normal,
833 shade_color: None,
834 emphasis: EmphasisType::None,
835 ratio: 100,
836 spacing: 0,
837 rel_sz: 100,
838 offset: 0,
839 use_kerning: false,
840 use_font_space: false,
841 char_border_fill_id: None,
842 };
843 let yaml = serde_yaml::to_string(&original).unwrap();
844 let back: CharShape = serde_yaml::from_str(&yaml).unwrap();
845 assert_eq!(original, back);
846 }
847
848 #[test]
849 fn char_shape_yaml_contains_human_readable() {
850 let cs = CharShape {
851 font: "Arial".into(),
852 size: HwpUnit::from_pt(12.0).unwrap(),
853 bold: false,
854 italic: true,
855 color: Color::RED,
856 underline_type: UnderlineType::None,
857 underline_color: None,
858 strikeout_shape: StrikeoutShape::None,
859 strikeout_color: None,
860 outline: OutlineType::None,
861 shadow: ShadowType::None,
862 emboss: EmbossType::None,
863 engrave: EngraveType::None,
864 vertical_position: VerticalPosition::Normal,
865 shade_color: None,
866 emphasis: EmphasisType::None,
867 ratio: 100,
868 spacing: 0,
869 rel_sz: 100,
870 offset: 0,
871 use_kerning: false,
872 use_font_space: false,
873 char_border_fill_id: None,
874 };
875 let yaml = serde_yaml::to_string(&cs).unwrap();
876 assert!(yaml.contains("12pt"), "Expected '12pt' in: {yaml}");
877 assert!(yaml.contains("#FF0000"), "Expected '#FF0000' in: {yaml}");
878 }
879
880 #[test]
881 fn para_shape_serde_roundtrip() {
882 let original = ParaShape {
883 alignment: Alignment::Justify,
884 line_spacing_type: LineSpacingType::Percentage,
885 line_spacing_value: 170.0,
886 space_before: HwpUnit::from_pt(6.0).unwrap(),
887 space_after: HwpUnit::from_pt(6.0).unwrap(),
888 indent_left: HwpUnit::ZERO,
889 indent_right: HwpUnit::ZERO,
890 indent_first_line: HwpUnit::ZERO,
891 break_type: BreakType::None,
892 keep_with_next: false,
893 keep_lines_together: false,
894 widow_orphan: true,
895 border_fill_id: None,
896 heading_type: HeadingType::None,
897 };
898 let yaml = serde_yaml::to_string(&original).unwrap();
899 let back: ParaShape = serde_yaml::from_str(&yaml).unwrap();
900 assert_eq!(original, back);
901 }
902
903 #[test]
904 fn partial_char_shape_from_yaml() {
905 let yaml = "font: 한컴바탕\nsize: 16pt\nbold: true\ncolor: '#003366'\n";
906 let partial: PartialCharShape = serde_yaml::from_str(yaml).unwrap();
907 assert_eq!(partial.font, Some("한컴바탕".into()));
908 assert_eq!(partial.size, Some(HwpUnit::from_pt(16.0).unwrap()));
909 assert_eq!(partial.bold, Some(true));
910 assert_eq!(partial.color, Some(Color::from_rgb(0x00, 0x33, 0x66)));
911 assert!(partial.italic.is_none());
912 }
913
914 #[test]
915 fn partial_para_shape_from_yaml() {
916 let yaml = "alignment: Justify\nline_spacing:\n value: '170%'\nspacing:\n before: '6pt'\n after: '6pt'\n";
917 let partial: PartialParaShape = serde_yaml::from_str(yaml).unwrap();
918 assert_eq!(partial.alignment, Some(Alignment::Justify));
919 assert_eq!(partial.line_spacing.unwrap().value, Some(170.0));
920 assert_eq!(partial.spacing.unwrap().before, Some(HwpUnit::from_pt(6.0).unwrap()));
921 }
922
923 #[test]
924 fn partial_style_from_yaml() {
925 let yaml = "char_shape:\n font: Arial\n size: '10pt'\npara_shape:\n alignment: Left\n";
926 let style: PartialStyle = serde_yaml::from_str(yaml).unwrap();
927 assert_eq!(style.char_shape.as_ref().unwrap().font, Some("Arial".into()));
928 assert_eq!(style.para_shape.as_ref().unwrap().alignment, Some(Alignment::Left));
929 }
930
931 #[test]
932 fn spacing_from_yaml() {
933 let yaml = "before: '6pt'\nafter: '12pt'\n";
934 let spacing: Spacing = serde_yaml::from_str(yaml).unwrap();
935 assert_eq!(spacing.before, Some(HwpUnit::from_pt(6.0).unwrap()));
936 assert_eq!(spacing.after, Some(HwpUnit::from_pt(12.0).unwrap()));
937 }
938
939 #[test]
940 fn indent_from_yaml() {
941 let yaml = "left: '20mm'\nfirst_line: '10pt'\n";
942 let indent: Indent = serde_yaml::from_str(yaml).unwrap();
943 assert_eq!(indent.left, Some(HwpUnit::from_mm(20.0).unwrap()));
944 assert_eq!(indent.first_line, Some(HwpUnit::from_pt(10.0).unwrap()));
945 assert!(indent.right.is_none());
946 }
947
948 #[test]
949 fn line_spacing_from_yaml() {
950 let yaml = "spacing_type: Percentage\nvalue: '160%'\n";
951 let ls: LineSpacing = serde_yaml::from_str(yaml).unwrap();
952 assert_eq!(ls.spacing_type, Some(LineSpacingType::Percentage));
953 assert_eq!(ls.value, Some(160.0));
954 }
955}