use serde::{Deserialize, Serialize};
use hwpforge_blueprint::registry::StyleRegistry;
use hwpforge_core::{NumberingDef, TabDef};
use hwpforge_foundation::{
Alignment, BorderFillIndex, BreakType, CharShapeIndex, Color, EmbossType, EmphasisType,
EngraveType, FontIndex, HeadingType, HwpUnit, LineSpacingType, OutlineType, ParaShapeIndex,
ShadowType, StrikeoutShape, UnderlineType, VerticalPosition, WordBreakType,
};
use crate::default_styles::HancomStyleSet;
use crate::error::{HwpxError, HwpxResult};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HwpxFont {
pub id: u32,
pub face_name: String,
pub lang: String,
}
impl HwpxFont {
pub fn new(id: u32, face_name: impl Into<String>, lang: impl Into<String>) -> Self {
Self { id, face_name: face_name.into(), lang: lang.into() }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HwpxFontRef {
pub hangul: FontIndex,
pub latin: FontIndex,
pub hanja: FontIndex,
pub japanese: FontIndex,
pub other: FontIndex,
pub symbol: FontIndex,
pub user: FontIndex,
}
impl Default for HwpxFontRef {
fn default() -> Self {
let zero = FontIndex::new(0);
Self {
hangul: zero,
latin: zero,
hanja: zero,
japanese: zero,
other: zero,
symbol: zero,
user: zero,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HwpxCharShape {
pub font_ref: HwpxFontRef,
pub height: HwpUnit,
pub text_color: Color,
pub shade_color: Option<Color>,
pub bold: bool,
pub italic: bool,
pub underline_type: UnderlineType,
pub underline_color: Option<Color>,
pub strikeout_shape: StrikeoutShape,
pub strikeout_color: Option<Color>,
pub vertical_position: VerticalPosition,
pub outline_type: OutlineType,
pub shadow_type: ShadowType,
pub emboss_type: EmbossType,
pub engrave_type: EngraveType,
pub emphasis: EmphasisType,
pub ratio: i32,
pub spacing: i32,
pub rel_sz: i32,
pub char_offset: i32,
pub use_kerning: bool,
pub use_font_space: bool,
pub border_fill_id: Option<u32>,
}
impl Default for HwpxCharShape {
fn default() -> Self {
Self {
font_ref: HwpxFontRef::default(),
height: HwpUnit::new(1000).unwrap(), text_color: Color::BLACK,
shade_color: None,
bold: false,
italic: false,
underline_type: UnderlineType::None,
underline_color: None,
strikeout_shape: StrikeoutShape::None,
strikeout_color: None,
vertical_position: VerticalPosition::Normal,
outline_type: OutlineType::None,
shadow_type: ShadowType::None,
emboss_type: EmbossType::None,
engrave_type: EngraveType::None,
emphasis: EmphasisType::None,
ratio: 100,
spacing: 0,
rel_sz: 100,
char_offset: 0,
use_kerning: false,
use_font_space: false,
border_fill_id: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HwpxStyle {
pub id: u32,
pub style_type: String,
pub name: String,
pub eng_name: String,
pub para_pr_id_ref: u32,
pub char_pr_id_ref: u32,
pub next_style_id_ref: u32,
pub lang_id: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HwpxParaShape {
pub alignment: Alignment,
pub margin_left: HwpUnit,
pub margin_right: HwpUnit,
pub indent: HwpUnit,
pub spacing_before: HwpUnit,
pub spacing_after: HwpUnit,
pub line_spacing: i32,
pub line_spacing_type: LineSpacingType,
pub break_type: BreakType,
pub keep_with_next: bool,
pub keep_lines_together: bool,
pub widow_orphan: bool,
pub break_latin_word: WordBreakType,
pub break_non_latin_word: WordBreakType,
pub border_fill_id: Option<BorderFillIndex>,
pub heading_type: HeadingType,
pub heading_id_ref: u32,
pub heading_level: u32,
pub tab_pr_id_ref: u32,
pub condense: u32,
}
impl Default for HwpxParaShape {
fn default() -> Self {
Self {
alignment: Alignment::Left,
margin_left: HwpUnit::ZERO,
margin_right: HwpUnit::ZERO,
indent: HwpUnit::ZERO,
spacing_before: HwpUnit::ZERO,
spacing_after: HwpUnit::ZERO,
line_spacing: 160,
line_spacing_type: LineSpacingType::Percentage,
break_type: BreakType::None,
keep_with_next: false,
keep_lines_together: false,
widow_orphan: true, break_latin_word: WordBreakType::KeepWord,
break_non_latin_word: WordBreakType::KeepWord,
border_fill_id: None,
heading_type: HeadingType::None,
heading_id_ref: 0,
heading_level: 0,
tab_pr_id_ref: 0,
condense: 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct HwpxBorderFill {
pub id: u32,
pub three_d: bool,
pub shadow: bool,
pub center_line: String,
pub left: HwpxBorderLine,
pub right: HwpxBorderLine,
pub top: HwpxBorderLine,
pub bottom: HwpxBorderLine,
pub diagonal: HwpxBorderLine,
pub slash_type: String,
pub back_slash_type: String,
pub fill: Option<HwpxFill>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HwpxBorderLine {
pub line_type: String,
pub width: String,
pub color: String,
}
impl Default for HwpxBorderLine {
fn default() -> Self {
Self { line_type: "NONE".into(), width: "0.1 mm".into(), color: "#000000".into() }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum HwpxFill {
WinBrush {
face_color: String,
hatch_color: String,
alpha: String,
},
}
impl HwpxBorderFill {
pub fn default_page_border() -> Self {
let none_border = HwpxBorderLine::default(); Self {
id: 1,
three_d: false,
shadow: false,
center_line: "NONE".into(),
left: none_border.clone(),
right: none_border.clone(),
top: none_border.clone(),
bottom: none_border.clone(),
diagonal: HwpxBorderLine { line_type: "SOLID".into(), ..HwpxBorderLine::default() },
slash_type: "NONE".into(),
back_slash_type: "NONE".into(),
fill: None,
}
}
pub fn default_char_background() -> Self {
let none_border = HwpxBorderLine::default();
Self {
id: 2,
three_d: false,
shadow: false,
center_line: "NONE".into(),
left: none_border.clone(),
right: none_border.clone(),
top: none_border.clone(),
bottom: none_border.clone(),
diagonal: HwpxBorderLine { line_type: "SOLID".into(), ..HwpxBorderLine::default() },
slash_type: "NONE".into(),
back_slash_type: "NONE".into(),
fill: Some(HwpxFill::WinBrush {
face_color: "none".into(),
hatch_color: "#FF000000".into(),
alpha: "0".into(),
}),
}
}
pub fn default_table_border() -> Self {
let solid_border = HwpxBorderLine {
line_type: "SOLID".into(),
width: "0.12 mm".into(),
color: "#000000".into(),
};
Self {
id: 3,
three_d: false,
shadow: false,
center_line: "NONE".into(),
left: solid_border.clone(),
right: solid_border.clone(),
top: solid_border.clone(),
bottom: solid_border.clone(),
diagonal: HwpxBorderLine { line_type: "SOLID".into(), ..HwpxBorderLine::default() },
slash_type: "NONE".into(),
back_slash_type: "NONE".into(),
fill: None,
}
}
}
pub(crate) fn default_char_shapes_modern() -> [HwpxCharShape; 7] {
let batang = FontIndex::new(1); let dotum = FontIndex::new(0);
let batang_ref = HwpxFontRef {
hangul: batang,
latin: batang,
hanja: batang,
japanese: batang,
other: batang,
symbol: batang,
user: batang,
};
let dotum_ref = HwpxFontRef {
hangul: dotum,
latin: dotum,
hanja: dotum,
japanese: dotum,
other: dotum,
symbol: dotum,
user: dotum,
};
let base = HwpxCharShape {
font_ref: batang_ref,
height: HwpUnit::new(1000).unwrap(), text_color: Color::BLACK,
shade_color: None,
bold: false,
italic: false,
underline_type: UnderlineType::None,
underline_color: None,
strikeout_shape: StrikeoutShape::None,
strikeout_color: None,
vertical_position: VerticalPosition::Normal,
outline_type: OutlineType::None,
shadow_type: ShadowType::None,
emboss_type: EmbossType::None,
engrave_type: EngraveType::None,
emphasis: EmphasisType::None,
ratio: 100,
spacing: 0,
rel_sz: 100,
char_offset: 0,
use_kerning: false,
use_font_space: false,
border_fill_id: None,
};
[
base.clone(),
HwpxCharShape { font_ref: dotum_ref, ..base.clone() },
HwpxCharShape { font_ref: dotum_ref, height: HwpUnit::new(900).unwrap(), ..base.clone() },
HwpxCharShape { height: HwpUnit::new(900).unwrap(), ..base.clone() },
HwpxCharShape { font_ref: dotum_ref, height: HwpUnit::new(900).unwrap(), ..base.clone() },
HwpxCharShape {
font_ref: dotum_ref,
height: HwpUnit::new(1600).unwrap(),
text_color: Color::from_rgb(0x2E, 0x74, 0xB5),
..base.clone()
},
HwpxCharShape { font_ref: dotum_ref, height: HwpUnit::new(1100).unwrap(), ..base },
]
}
pub(crate) fn default_para_shapes_modern() -> [HwpxParaShape; 20] {
let justify = Alignment::Justify;
let left = Alignment::Left;
let base = HwpxParaShape {
alignment: justify,
margin_left: HwpUnit::ZERO,
margin_right: HwpUnit::ZERO,
indent: HwpUnit::ZERO,
spacing_before: HwpUnit::ZERO,
spacing_after: HwpUnit::ZERO,
line_spacing: 160,
line_spacing_type: LineSpacingType::Percentage,
break_type: BreakType::None,
keep_with_next: false,
keep_lines_together: false,
widow_orphan: false,
break_latin_word: WordBreakType::KeepWord,
break_non_latin_word: WordBreakType::KeepWord,
border_fill_id: None,
heading_type: HeadingType::None,
heading_id_ref: 0,
heading_level: 0,
tab_pr_id_ref: 0,
condense: 0,
};
[
base.clone(),
HwpxParaShape { margin_left: HwpUnit::new(1500).unwrap(), ..base.clone() },
HwpxParaShape {
margin_left: HwpUnit::new(1000).unwrap(),
heading_type: HeadingType::Outline,
heading_id_ref: 1,
heading_level: 1,
tab_pr_id_ref: 1,
condense: 20,
..base.clone()
},
HwpxParaShape {
margin_left: HwpUnit::new(2000).unwrap(),
heading_type: HeadingType::Outline,
heading_id_ref: 1,
heading_level: 2,
tab_pr_id_ref: 1,
condense: 20,
..base.clone()
},
HwpxParaShape {
margin_left: HwpUnit::new(3000).unwrap(),
heading_type: HeadingType::Outline,
heading_id_ref: 1,
heading_level: 3,
tab_pr_id_ref: 1,
condense: 20,
..base.clone()
},
HwpxParaShape {
margin_left: HwpUnit::new(4000).unwrap(),
heading_type: HeadingType::Outline,
heading_id_ref: 1,
heading_level: 4,
tab_pr_id_ref: 1,
condense: 20,
..base.clone()
},
HwpxParaShape {
margin_left: HwpUnit::new(5000).unwrap(),
heading_type: HeadingType::Outline,
heading_id_ref: 1,
heading_level: 5,
tab_pr_id_ref: 1,
condense: 20,
..base.clone()
},
HwpxParaShape {
margin_left: HwpUnit::new(6000).unwrap(),
heading_type: HeadingType::Outline,
heading_id_ref: 1,
heading_level: 6,
tab_pr_id_ref: 1,
condense: 20,
..base.clone()
},
HwpxParaShape {
margin_left: HwpUnit::new(7000).unwrap(),
heading_type: HeadingType::Outline,
heading_id_ref: 1,
heading_level: 7,
tab_pr_id_ref: 1,
condense: 20,
..base.clone()
},
HwpxParaShape { line_spacing: 150, ..base.clone() },
HwpxParaShape { indent: HwpUnit::new(-1310).unwrap(), line_spacing: 130, ..base.clone() },
HwpxParaShape { alignment: left, line_spacing: 130, ..base.clone() },
HwpxParaShape {
alignment: left,
spacing_before: HwpUnit::new(1200).unwrap(),
spacing_after: HwpUnit::new(300).unwrap(),
..base.clone()
},
HwpxParaShape {
alignment: left,
spacing_after: HwpUnit::new(700).unwrap(),
..base.clone()
},
HwpxParaShape {
alignment: left,
margin_left: HwpUnit::new(1100).unwrap(),
spacing_after: HwpUnit::new(700).unwrap(),
..base.clone()
},
HwpxParaShape {
alignment: left,
margin_left: HwpUnit::new(2200).unwrap(),
spacing_after: HwpUnit::new(700).unwrap(),
..base.clone()
},
HwpxParaShape {
margin_left: HwpUnit::new(9000).unwrap(),
heading_type: HeadingType::Outline,
heading_id_ref: 1,
heading_level: 9,
tab_pr_id_ref: 1,
condense: 20,
..base.clone()
},
HwpxParaShape {
margin_left: HwpUnit::new(10000).unwrap(),
heading_type: HeadingType::Outline,
heading_id_ref: 1,
heading_level: 10,
tab_pr_id_ref: 1,
condense: 20,
..base.clone()
},
HwpxParaShape {
margin_left: HwpUnit::new(8000).unwrap(),
heading_type: HeadingType::Outline,
heading_id_ref: 1,
heading_level: 8,
tab_pr_id_ref: 1,
condense: 20,
..base.clone()
},
HwpxParaShape { line_spacing: 150, spacing_after: HwpUnit::new(800).unwrap(), ..base },
]
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct HwpxStyleStore {
style_set: HancomStyleSet,
fonts: Vec<HwpxFont>,
char_shapes: Vec<HwpxCharShape>,
para_shapes: Vec<HwpxParaShape>,
styles: Vec<HwpxStyle>,
border_fills: Vec<HwpxBorderFill>,
numberings: Vec<NumberingDef>,
tabs: Vec<TabDef>,
}
impl HwpxStyleStore {
pub fn new() -> Self {
Self::default()
}
pub fn with_default_fonts(font_name: &str) -> Self {
let mut store: Self = Self::new();
let langs: [&str; 7] = ["HANGUL", "LATIN", "HANJA", "JAPANESE", "OTHER", "SYMBOL", "USER"];
for (idx, &lang) in langs.iter().enumerate() {
store.push_font(HwpxFont::new(idx as u32, font_name, lang));
}
store
}
pub fn style_set(&self) -> HancomStyleSet {
self.style_set
}
pub fn from_registry(registry: &StyleRegistry) -> Self {
Self::from_registry_with(registry, HancomStyleSet::default())
}
pub fn from_registry_with(registry: &StyleRegistry, style_set: HancomStyleSet) -> Self {
let mut store = Self { style_set, ..Self::default() };
let has_fonts = !registry.fonts.is_empty();
let default_font = if has_fonts {
registry.fonts[0].as_str()
} else {
"함초롬바탕" };
const FONT_LANGS: &[&str] =
&["HANGUL", "LATIN", "HANJA", "JAPANESE", "OTHER", "SYMBOL", "USER"];
if has_fonts {
for &lang in FONT_LANGS {
for (i, font_id) in registry.fonts.iter().enumerate() {
store.push_font(HwpxFont {
id: i as u32,
face_name: font_id.as_str().to_string(),
lang: lang.to_string(),
});
}
}
} else {
for &lang in FONT_LANGS {
store.push_font(HwpxFont {
id: 0,
face_name: default_font.to_string(),
lang: lang.to_string(),
});
}
}
for cs in default_char_shapes_modern() {
store.push_char_shape(cs);
}
for ps in default_para_shapes_modern() {
store.push_para_shape(ps);
}
let char_shape_offset = store.char_shape_count(); let para_shape_offset = store.para_shape_count();
for cs in ®istry.char_shapes {
let font_idx = registry
.fonts
.iter()
.position(|f| f.as_str() == cs.font)
.map(FontIndex::new)
.unwrap_or(FontIndex::new(0));
let font_ref = HwpxFontRef {
hangul: font_idx,
latin: font_idx,
hanja: font_idx,
japanese: font_idx,
other: font_idx,
symbol: font_idx,
user: font_idx,
};
store.push_char_shape(HwpxCharShape {
font_ref,
height: cs.size,
text_color: cs.color,
shade_color: cs.shade_color,
bold: cs.bold,
italic: cs.italic,
underline_type: cs.underline_type,
underline_color: cs.underline_color,
strikeout_shape: cs.strikeout_shape,
strikeout_color: cs.strikeout_color,
vertical_position: cs.vertical_position,
outline_type: cs.outline,
shadow_type: cs.shadow,
emboss_type: cs.emboss,
engrave_type: cs.engrave,
emphasis: cs.emphasis,
ratio: cs.ratio,
spacing: cs.spacing,
rel_sz: cs.rel_sz,
char_offset: cs.offset,
use_kerning: cs.use_kerning,
use_font_space: cs.use_font_space,
border_fill_id: cs.char_border_fill_id,
});
}
for ps in ®istry.para_shapes {
store.push_para_shape(HwpxParaShape {
alignment: ps.alignment,
margin_left: ps.indent_left,
margin_right: ps.indent_right,
indent: ps.indent_first_line,
spacing_before: ps.space_before,
spacing_after: ps.space_after,
line_spacing: ps.line_spacing_value.round() as i32,
line_spacing_type: ps.line_spacing_type,
break_type: ps.break_type,
keep_with_next: ps.keep_with_next,
keep_lines_together: ps.keep_lines_together,
widow_orphan: ps.widow_orphan,
break_latin_word: WordBreakType::KeepWord,
break_non_latin_word: WordBreakType::KeepWord,
border_fill_id: ps.border_fill_id,
heading_type: HeadingType::None,
heading_id_ref: 0,
heading_level: 0,
tab_pr_id_ref: 0,
condense: 0,
});
}
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();
for (idx, entry) in defaults.iter().enumerate() {
let next_style_id_ref = if entry.is_char_style() { 0 } else { idx as u32 };
store.push_style(HwpxStyle {
id: idx as u32,
style_type: entry.style_type.to_string(),
name: entry.name.to_string(),
eng_name: entry.eng_name.to_string(),
para_pr_id_ref: entry.para_pr_group as u32,
char_pr_id_ref: entry.char_pr_group as u32,
next_style_id_ref,
lang_id: 1042, });
}
let style_offset = defaults.len();
for (i, (name, entry)) in registry.style_entries.iter().enumerate() {
store.push_style(HwpxStyle {
id: (style_offset + i) as u32,
style_type: "PARA".to_string(),
name: name.clone(),
eng_name: name.clone(),
para_pr_id_ref: (entry.para_shape_id.get() + para_shape_offset) as u32,
char_pr_id_ref: (entry.char_shape_id.get() + char_shape_offset) as u32,
next_style_id_ref: 0,
lang_id: 1042, });
}
store
}
pub fn push_font(&mut self, font: HwpxFont) -> FontIndex {
let idx = FontIndex::new(self.fonts.len());
self.fonts.push(font);
idx
}
pub fn font(&self, index: FontIndex) -> HwpxResult<&HwpxFont> {
self.fonts.get(index.get()).ok_or_else(|| HwpxError::IndexOutOfBounds {
kind: "font",
index: index.get() as u32,
max: self.fonts.len() as u32,
})
}
pub fn font_count(&self) -> usize {
self.fonts.len()
}
pub fn push_char_shape(&mut self, shape: HwpxCharShape) -> CharShapeIndex {
let idx = CharShapeIndex::new(self.char_shapes.len());
self.char_shapes.push(shape);
idx
}
pub fn char_shape(&self, index: CharShapeIndex) -> HwpxResult<&HwpxCharShape> {
self.char_shapes.get(index.get()).ok_or_else(|| HwpxError::IndexOutOfBounds {
kind: "char_shape",
index: index.get() as u32,
max: self.char_shapes.len() as u32,
})
}
pub fn char_shape_count(&self) -> usize {
self.char_shapes.len()
}
pub fn push_para_shape(&mut self, shape: HwpxParaShape) -> ParaShapeIndex {
let idx = ParaShapeIndex::new(self.para_shapes.len());
self.para_shapes.push(shape);
idx
}
pub fn para_shape(&self, index: ParaShapeIndex) -> HwpxResult<&HwpxParaShape> {
self.para_shapes.get(index.get()).ok_or_else(|| HwpxError::IndexOutOfBounds {
kind: "para_shape",
index: index.get() as u32,
max: self.para_shapes.len() as u32,
})
}
pub fn para_shape_count(&self) -> usize {
self.para_shapes.len()
}
pub fn iter_fonts(&self) -> impl Iterator<Item = &HwpxFont> {
self.fonts.iter()
}
pub fn iter_char_shapes(&self) -> impl Iterator<Item = &HwpxCharShape> {
self.char_shapes.iter()
}
pub fn iter_para_shapes(&self) -> impl Iterator<Item = &HwpxParaShape> {
self.para_shapes.iter()
}
pub fn push_style(&mut self, style: HwpxStyle) {
self.styles.push(style);
}
pub fn style(&self, index: usize) -> HwpxResult<&HwpxStyle> {
self.styles.get(index).ok_or(HwpxError::IndexOutOfBounds {
kind: "style",
index: index as u32,
max: self.styles.len() as u32,
})
}
pub fn style_count(&self) -> usize {
self.styles.len()
}
pub fn iter_styles(&self) -> impl Iterator<Item = &HwpxStyle> {
self.styles.iter()
}
pub fn push_border_fill(&mut self, bf: HwpxBorderFill) -> u32 {
let id = bf.id;
self.border_fills.push(bf);
id
}
pub fn border_fill(&self, id: u32) -> HwpxResult<&HwpxBorderFill> {
self.border_fills.iter().find(|bf| bf.id == id).ok_or(HwpxError::IndexOutOfBounds {
kind: "border_fill",
index: id,
max: self.border_fills.len() as u32,
})
}
pub fn border_fill_count(&self) -> usize {
self.border_fills.len()
}
pub fn iter_border_fills(&self) -> impl Iterator<Item = &HwpxBorderFill> {
self.border_fills.iter()
}
pub fn push_numbering(&mut self, ndef: NumberingDef) {
self.numberings.push(ndef);
}
pub fn push_tab(&mut self, tab: TabDef) {
self.tabs.push(tab);
}
pub fn numbering_count(&self) -> u32 {
self.numberings.len() as u32
}
pub fn tab_count(&self) -> u32 {
self.tabs.len() as u32
}
pub fn iter_numberings(&self) -> impl Iterator<Item = &NumberingDef> {
self.numberings.iter()
}
pub fn iter_tabs(&self) -> impl Iterator<Item = &TabDef> {
self.tabs.iter()
}
}
#[allow(dead_code)]
const _: () = {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
fn assertions() {
assert_send::<HwpxStyleStore>();
assert_sync::<HwpxStyleStore>();
}
};
pub(crate) fn parse_hex_color(s: &str) -> Color {
let s = s.trim();
if s.is_empty() || s.eq_ignore_ascii_case("none") {
return Color::BLACK;
}
let hex = s.strip_prefix('#').unwrap_or(s);
if hex.len() != 6 {
return Color::BLACK;
}
let Ok(rgb) = u32::from_str_radix(hex, 16) else {
return Color::BLACK;
};
let r = ((rgb >> 16) & 0xFF) as u8;
let g = ((rgb >> 8) & 0xFF) as u8;
let b = (rgb & 0xFF) as u8;
Color::from_rgb(r, g, b)
}
pub(crate) fn parse_alignment(s: &str) -> Alignment {
if s.eq_ignore_ascii_case("LEFT") {
Alignment::Left
} else if s.eq_ignore_ascii_case("BOTH") || s.eq_ignore_ascii_case("JUSTIFY") {
Alignment::Justify
} else if s.eq_ignore_ascii_case("CENTER") {
Alignment::Center
} else if s.eq_ignore_ascii_case("RIGHT") {
Alignment::Right
} else if s.eq_ignore_ascii_case("DISTRIBUTE") {
Alignment::Distribute
} else if s.eq_ignore_ascii_case("DISTRIBUTE_FLUSH") {
Alignment::DistributeFlush
} else {
Alignment::Left
}
}
#[cfg(test)]
mod tests {
use super::*;
use hwpforge_blueprint::builtins::builtin_default;
use hwpforge_foundation::{CharShapeIndex, FontIndex, ParaShapeIndex};
#[test]
fn empty_store_returns_errors() {
let store = HwpxStyleStore::new();
assert!(store.font(FontIndex::new(0)).is_err());
assert!(store.char_shape(CharShapeIndex::new(0)).is_err());
assert!(store.para_shape(ParaShapeIndex::new(0)).is_err());
}
#[test]
fn push_and_get_font() {
let mut store = HwpxStyleStore::new();
let idx = store.push_font(HwpxFont {
id: 0,
face_name: "함초롬돋움".into(),
lang: "HANGUL".into(),
});
assert_eq!(idx.get(), 0);
let font = store.font(idx).unwrap();
assert_eq!(font.face_name, "함초롬돋움");
assert_eq!(font.lang, "HANGUL");
}
#[test]
fn push_and_get_char_shape() {
let mut store = HwpxStyleStore::new();
let shape = HwpxCharShape {
height: HwpUnit::new(1000).unwrap(),
text_color: Color::from_rgb(255, 0, 0),
bold: true,
..Default::default()
};
let idx = store.push_char_shape(shape);
let cs = store.char_shape(idx).unwrap();
assert_eq!(cs.height.as_i32(), 1000);
assert_eq!(cs.text_color.red(), 255);
assert!(cs.bold);
assert!(!cs.italic);
}
#[test]
fn push_and_get_para_shape() {
let mut store = HwpxStyleStore::new();
let shape =
HwpxParaShape { alignment: Alignment::Center, line_spacing: 200, ..Default::default() };
let idx = store.push_para_shape(shape);
let ps = store.para_shape(idx).unwrap();
assert_eq!(ps.alignment, Alignment::Center);
assert_eq!(ps.line_spacing, 200);
}
#[test]
fn index_out_of_bounds_error() {
let store = HwpxStyleStore::new();
let err = store.char_shape(CharShapeIndex::new(42)).unwrap_err();
match err {
HwpxError::IndexOutOfBounds { kind, index, max } => {
assert_eq!(kind, "char_shape");
assert_eq!(index, 42);
assert_eq!(max, 0);
}
_ => panic!("expected IndexOutOfBounds"),
}
}
#[test]
fn multiple_items_sequential_indices() {
let mut store = HwpxStyleStore::new();
for i in 0..5 {
let idx = store.push_font(HwpxFont {
id: i,
face_name: format!("Font{i}"),
lang: "LATIN".into(),
});
assert_eq!(idx.get(), i as usize);
}
assert_eq!(store.font_count(), 5);
assert_eq!(store.font(FontIndex::new(3)).unwrap().face_name, "Font3");
}
#[test]
fn count_methods() {
let mut store = HwpxStyleStore::new();
assert_eq!(store.font_count(), 0);
assert_eq!(store.char_shape_count(), 0);
assert_eq!(store.para_shape_count(), 0);
store.push_font(HwpxFont { id: 0, face_name: "A".into(), lang: "LATIN".into() });
store.push_char_shape(HwpxCharShape::default());
store.push_char_shape(HwpxCharShape::default());
store.push_para_shape(HwpxParaShape::default());
assert_eq!(store.font_count(), 1);
assert_eq!(store.char_shape_count(), 2);
assert_eq!(store.para_shape_count(), 1);
}
#[test]
fn iter_fonts_yields_all() {
let mut store = HwpxStyleStore::new();
for i in 0..3 {
store.push_font(HwpxFont {
id: i,
face_name: format!("Font{i}"),
lang: "LATIN".into(),
});
}
let names: Vec<&str> = store.iter_fonts().map(|f| f.face_name.as_str()).collect();
assert_eq!(names, vec!["Font0", "Font1", "Font2"]);
}
#[test]
fn iter_char_shapes_yields_all() {
let mut store = HwpxStyleStore::new();
store.push_char_shape(HwpxCharShape { bold: true, ..Default::default() });
store.push_char_shape(HwpxCharShape { italic: true, ..Default::default() });
let styles: Vec<(bool, bool)> =
store.iter_char_shapes().map(|c| (c.bold, c.italic)).collect();
assert_eq!(styles, vec![(true, false), (false, true)]);
}
#[test]
fn iter_para_shapes_yields_all() {
let mut store = HwpxStyleStore::new();
store.push_para_shape(HwpxParaShape { line_spacing: 130, ..Default::default() });
store.push_para_shape(HwpxParaShape { line_spacing: 200, ..Default::default() });
let spacings: Vec<i32> = store.iter_para_shapes().map(|p| p.line_spacing).collect();
assert_eq!(spacings, vec![130, 200]);
}
#[test]
fn iter_empty_store() {
let store = HwpxStyleStore::new();
assert_eq!(store.iter_fonts().count(), 0);
assert_eq!(store.iter_char_shapes().count(), 0);
assert_eq!(store.iter_para_shapes().count(), 0);
}
#[test]
fn font_ref_default_all_zero() {
let r = HwpxFontRef::default();
assert_eq!(r.hangul.get(), 0);
assert_eq!(r.latin.get(), 0);
assert_eq!(r.hanja.get(), 0);
assert_eq!(r.japanese.get(), 0);
assert_eq!(r.other.get(), 0);
assert_eq!(r.symbol.get(), 0);
assert_eq!(r.user.get(), 0);
}
#[test]
fn char_shape_default_values() {
let cs = HwpxCharShape::default();
assert_eq!(cs.height, HwpUnit::new(1000).unwrap()); assert_eq!(cs.text_color, Color::BLACK);
assert_eq!(cs.shade_color, None);
assert!(!cs.bold);
assert!(!cs.italic);
assert_eq!(cs.underline_type, UnderlineType::None);
assert_eq!(cs.underline_color, None);
assert_eq!(cs.strikeout_shape, StrikeoutShape::None);
assert_eq!(cs.strikeout_color, None);
}
#[test]
fn para_shape_default_values() {
let ps = HwpxParaShape::default();
assert_eq!(ps.alignment, Alignment::Left);
assert_eq!(ps.margin_left, HwpUnit::ZERO);
assert_eq!(ps.indent, HwpUnit::ZERO);
assert_eq!(ps.line_spacing, 160);
assert_eq!(ps.line_spacing_type, LineSpacingType::Percentage);
}
#[test]
fn parse_hex_color_valid() {
let c = parse_hex_color("#FF0000");
assert_eq!(c.red(), 255);
assert_eq!(c.green(), 0);
assert_eq!(c.blue(), 0);
}
#[test]
fn parse_hex_color_lowercase() {
let c = parse_hex_color("#00ff00");
assert_eq!(c.green(), 255);
}
#[test]
fn parse_hex_color_no_hash() {
let c = parse_hex_color("0000FF");
assert_eq!(c.blue(), 255);
}
#[test]
fn parse_hex_color_none_returns_black() {
assert_eq!(parse_hex_color("none"), Color::BLACK);
assert_eq!(parse_hex_color("NONE"), Color::BLACK);
}
#[test]
fn parse_hex_color_empty_returns_black() {
assert_eq!(parse_hex_color(""), Color::BLACK);
}
#[test]
fn parse_hex_color_invalid_returns_black() {
assert_eq!(parse_hex_color("#GGHHII"), Color::BLACK);
assert_eq!(parse_hex_color("#FFF"), Color::BLACK); assert_eq!(parse_hex_color("garbage"), Color::BLACK);
}
#[test]
fn parse_hex_color_white() {
let c = parse_hex_color("#FFFFFF");
assert_eq!(c, Color::WHITE);
}
#[test]
fn parse_alignment_standard() {
assert_eq!(parse_alignment("LEFT"), Alignment::Left);
assert_eq!(parse_alignment("CENTER"), Alignment::Center);
assert_eq!(parse_alignment("RIGHT"), Alignment::Right);
assert_eq!(parse_alignment("JUSTIFY"), Alignment::Justify);
}
#[test]
fn parse_alignment_both_maps_to_justify() {
assert_eq!(parse_alignment("BOTH"), Alignment::Justify);
}
#[test]
fn parse_alignment_case_insensitive() {
assert_eq!(parse_alignment("center"), Alignment::Center);
assert_eq!(parse_alignment("Right"), Alignment::Right);
}
#[test]
fn parse_alignment_distribute() {
assert_eq!(parse_alignment("DISTRIBUTE"), Alignment::Distribute);
assert_eq!(parse_alignment("distribute"), Alignment::Distribute);
assert_eq!(parse_alignment("DISTRIBUTE_FLUSH"), Alignment::DistributeFlush);
assert_eq!(parse_alignment("distribute_flush"), Alignment::DistributeFlush);
}
#[test]
fn parse_alignment_unknown_defaults_left() {
assert_eq!(parse_alignment("DISTRIBUTED"), Alignment::Left);
assert_eq!(parse_alignment(""), Alignment::Left);
}
#[test]
fn push_and_get_style() {
let mut store = HwpxStyleStore::new();
let style = HwpxStyle {
id: 0,
style_type: "PARA".into(),
name: "바탕글".into(),
eng_name: "Normal".into(),
para_pr_id_ref: 0,
char_pr_id_ref: 0,
next_style_id_ref: 0,
lang_id: 1042,
};
store.push_style(style);
assert_eq!(store.style_count(), 1);
let s = store.style(0).unwrap();
assert_eq!(s.name, "바탕글");
assert_eq!(s.eng_name, "Normal");
assert_eq!(s.style_type, "PARA");
}
#[test]
fn style_index_out_of_bounds() {
let store = HwpxStyleStore::new();
let err = store.style(0).unwrap_err();
match err {
HwpxError::IndexOutOfBounds { kind, index, max } => {
assert_eq!(kind, "style");
assert_eq!(index, 0);
assert_eq!(max, 0);
}
_ => panic!("expected IndexOutOfBounds"),
}
}
#[test]
fn iter_styles_yields_all() {
let mut store = HwpxStyleStore::new();
store.push_style(HwpxStyle {
id: 0,
style_type: "PARA".into(),
name: "바탕글".into(),
eng_name: "Normal".into(),
para_pr_id_ref: 0,
char_pr_id_ref: 0,
next_style_id_ref: 0,
lang_id: 1042,
});
store.push_style(HwpxStyle {
id: 1,
style_type: "CHAR".into(),
name: "본문".into(),
eng_name: "Body".into(),
para_pr_id_ref: 1,
char_pr_id_ref: 1,
next_style_id_ref: 1,
lang_id: 1042,
});
let names: Vec<&str> = store.iter_styles().map(|s| s.name.as_str()).collect();
assert_eq!(names, vec!["바탕글", "본문"]);
}
#[test]
fn from_registry_empty_produces_empty_store() {
let registry: StyleRegistry = serde_json::from_str(
r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
)
.unwrap();
let store = HwpxStyleStore::from_registry(®istry);
assert_eq!(store.font_count(), 7);
assert_eq!(store.char_shape_count(), 7); assert_eq!(store.para_shape_count(), 20); assert_eq!(store.style_count(), 22);
}
#[test]
fn from_registry_preserves_counts() {
let template = builtin_default().unwrap();
let registry = StyleRegistry::from_template(&template).unwrap();
let store = HwpxStyleStore::from_registry(®istry);
assert_eq!(store.font_count(), registry.font_count() * 7);
assert_eq!(store.char_shape_count(), 7 + registry.char_shape_count());
assert_eq!(store.para_shape_count(), 20 + registry.para_shape_count());
assert_eq!(store.style_count(), registry.style_count() + 22);
}
#[test]
fn from_registry_font_face_names_match() {
let template = builtin_default().unwrap();
let registry = StyleRegistry::from_template(&template).unwrap();
let store = HwpxStyleStore::from_registry(®istry);
let font_count = registry.font_count();
let langs = ["HANGUL", "LATIN", "HANJA", "JAPANESE", "OTHER", "SYMBOL", "USER"];
for (lang_idx, &lang) in langs.iter().enumerate() {
for (font_idx, font_id) in registry.fonts.iter().enumerate() {
let store_idx = lang_idx * font_count + font_idx;
let hwpx_font = store.font(FontIndex::new(store_idx)).unwrap();
assert_eq!(hwpx_font.face_name, font_id.as_str());
assert_eq!(hwpx_font.lang, lang);
}
}
}
#[test]
fn from_registry_char_shape_properties() {
let template = builtin_default().unwrap();
let registry = StyleRegistry::from_template(&template).unwrap();
let store = HwpxStyleStore::from_registry(®istry);
for (i, bp_cs) in registry.char_shapes.iter().enumerate() {
let hwpx_cs = store.char_shape(CharShapeIndex::new(7 + i)).unwrap();
assert_eq!(hwpx_cs.height, bp_cs.size);
assert_eq!(hwpx_cs.text_color, bp_cs.color);
assert_eq!(hwpx_cs.shade_color, bp_cs.shade_color);
assert_eq!(hwpx_cs.bold, bp_cs.bold);
assert_eq!(hwpx_cs.italic, bp_cs.italic);
assert_eq!(hwpx_cs.underline_type, bp_cs.underline_type);
assert_eq!(hwpx_cs.underline_color, bp_cs.underline_color);
assert_eq!(hwpx_cs.strikeout_shape, bp_cs.strikeout_shape);
assert_eq!(hwpx_cs.strikeout_color, bp_cs.strikeout_color);
assert_eq!(hwpx_cs.vertical_position, bp_cs.vertical_position);
assert_eq!(hwpx_cs.outline_type, bp_cs.outline);
assert_eq!(hwpx_cs.shadow_type, bp_cs.shadow);
assert_eq!(hwpx_cs.emboss_type, bp_cs.emboss);
assert_eq!(hwpx_cs.engrave_type, bp_cs.engrave);
}
}
#[test]
fn from_registry_para_shape_properties() {
let template = builtin_default().unwrap();
let registry = StyleRegistry::from_template(&template).unwrap();
let store = HwpxStyleStore::from_registry(®istry);
for (i, bp_ps) in registry.para_shapes.iter().enumerate() {
let hwpx_ps = store.para_shape(ParaShapeIndex::new(20 + i)).unwrap();
assert_eq!(hwpx_ps.alignment, bp_ps.alignment);
assert_eq!(hwpx_ps.margin_left, bp_ps.indent_left);
assert_eq!(hwpx_ps.margin_right, bp_ps.indent_right);
assert_eq!(hwpx_ps.indent, bp_ps.indent_first_line);
assert_eq!(hwpx_ps.spacing_before, bp_ps.space_before);
assert_eq!(hwpx_ps.spacing_after, bp_ps.space_after);
assert_eq!(hwpx_ps.line_spacing, bp_ps.line_spacing_value.round() as i32);
}
}
#[test]
fn from_registry_style_entries_reference_valid_indices() {
let template = builtin_default().unwrap();
let registry = StyleRegistry::from_template(&template).unwrap();
let store = HwpxStyleStore::from_registry(®istry);
for i in 0..store.style_count() {
let style = store.style(i).unwrap();
assert!(
style.style_type == "PARA" || style.style_type == "CHAR",
"unexpected style_type '{}' for style '{}'",
style.style_type,
style.name
);
assert!(
(style.char_pr_id_ref as usize) < store.char_shape_count(),
"char_pr_id_ref {} out of bounds for style '{}'",
style.char_pr_id_ref,
style.name
);
assert!(
(style.para_pr_id_ref as usize) < store.para_shape_count(),
"para_pr_id_ref {} out of bounds for style '{}'",
style.para_pr_id_ref,
style.name
);
}
}
#[test]
fn default_style_set_classic_count() {
assert_eq!(HancomStyleSet::Classic.count(), 18);
}
#[test]
fn default_style_set_modern_count() {
assert_eq!(HancomStyleSet::Modern.count(), 22);
}
#[test]
fn default_style_set_latest_count() {
assert_eq!(HancomStyleSet::Latest.count(), 23);
}
#[test]
fn default_style_set_modern_is_default() {
assert_eq!(HancomStyleSet::default(), HancomStyleSet::Modern);
}
#[test]
fn with_default_fonts_creates_seven_fonts() {
let store = HwpxStyleStore::with_default_fonts("함초롬돋움");
assert_eq!(store.font_count(), 7);
}
#[test]
fn with_default_fonts_all_names_match() {
let font_name = "나눔고딕";
let store = HwpxStyleStore::with_default_fonts(font_name);
for font in store.iter_fonts() {
assert_eq!(font.face_name, font_name);
}
}
#[test]
fn with_default_fonts_lang_groups_correct() {
let store = HwpxStyleStore::with_default_fonts("함초롬바탕");
let langs: Vec<&str> = store.iter_fonts().map(|f| f.lang.as_str()).collect();
assert_eq!(langs, vec!["HANGUL", "LATIN", "HANJA", "JAPANESE", "OTHER", "SYMBOL", "USER"]);
}
#[test]
fn from_registry_with_classic_style_set() {
let registry: StyleRegistry = serde_json::from_str(
r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
)
.unwrap();
let store = HwpxStyleStore::from_registry_with(®istry, HancomStyleSet::Classic);
assert_eq!(store.style_set(), HancomStyleSet::Classic);
assert_eq!(store.style_count(), 18);
assert_eq!(store.style(9).unwrap().name, "쪽 번호");
}
#[test]
fn modern_styles_match_golden_fixture() {
let styles = HancomStyleSet::Modern.default_styles();
assert_eq!(styles[9].name, "개요 8");
assert_eq!(styles[10].name, "개요 9");
assert_eq!(styles[11].name, "개요 10");
assert_eq!(styles[12].name, "쪽 번호");
assert_eq!(styles[12].style_type, "CHAR");
assert_eq!(styles[21].name, "캡션");
assert_eq!(styles[21].style_type, "PARA");
}
#[test]
fn default_border_fills_count() {
use hwpforge_blueprint::{builtins::builtin_default, registry::StyleRegistry};
let template = builtin_default().unwrap();
let registry = StyleRegistry::from_template(&template).unwrap();
let store = HwpxStyleStore::from_registry(®istry);
assert_eq!(store.border_fill_count(), 3, "from_registry produces exactly 3 default fills");
}
#[test]
fn default_border_fill_page() {
let bf = HwpxBorderFill::default_page_border();
assert_eq!(bf.id, 1);
assert!(!bf.three_d);
assert!(!bf.shadow);
assert_eq!(bf.center_line, "NONE");
assert_eq!(bf.left.line_type, "NONE");
assert_eq!(bf.right.line_type, "NONE");
assert_eq!(bf.top.line_type, "NONE");
assert_eq!(bf.bottom.line_type, "NONE");
assert_eq!(bf.diagonal.line_type, "SOLID");
assert!(bf.fill.is_none());
}
#[test]
fn default_border_fill_char() {
let bf = HwpxBorderFill::default_char_background();
assert_eq!(bf.id, 2);
assert!(bf.fill.is_some(), "char background must have a fill brush");
match bf.fill.as_ref().unwrap() {
HwpxFill::WinBrush { face_color, hatch_color, alpha } => {
assert_eq!(face_color, "none");
assert_eq!(hatch_color, "#FF000000");
assert_eq!(alpha, "0");
}
}
}
#[test]
fn default_border_fill_table() {
let bf = HwpxBorderFill::default_table_border();
assert_eq!(bf.id, 3);
assert_eq!(bf.left.line_type, "SOLID");
assert_eq!(bf.left.width, "0.12 mm");
assert_eq!(bf.right.line_type, "SOLID");
assert_eq!(bf.top.line_type, "SOLID");
assert_eq!(bf.bottom.line_type, "SOLID");
assert_eq!(bf.diagonal.line_type, "SOLID");
assert_eq!(bf.diagonal.width, "0.1 mm");
assert!(bf.fill.is_none());
}
#[test]
fn push_user_border_fill() {
let mut store = HwpxStyleStore::new();
let bf = HwpxBorderFill {
id: 4,
three_d: false,
shadow: false,
center_line: "NONE".into(),
left: HwpxBorderLine {
line_type: "DASH".into(),
width: "0.2 mm".into(),
color: "#FF0000".into(),
},
right: HwpxBorderLine::default(),
top: HwpxBorderLine::default(),
bottom: HwpxBorderLine::default(),
diagonal: HwpxBorderLine::default(),
slash_type: "NONE".into(),
back_slash_type: "NONE".into(),
fill: None,
};
let returned_id = store.push_border_fill(bf);
assert_eq!(returned_id, 4);
assert_eq!(store.border_fill_count(), 1);
let fetched = store.border_fill(4).unwrap();
assert_eq!(fetched.left.line_type, "DASH");
assert_eq!(fetched.left.width, "0.2 mm");
}
#[test]
fn border_fill_not_found_returns_error() {
let store = HwpxStyleStore::new();
assert!(store.border_fill(1).is_err());
}
#[test]
fn from_registry_border_fills_have_correct_ids() {
use hwpforge_blueprint::{builtins::builtin_default, registry::StyleRegistry};
let template = builtin_default().unwrap();
let registry = StyleRegistry::from_template(&template).unwrap();
let store = HwpxStyleStore::from_registry(®istry);
assert_eq!(store.border_fill(1).unwrap().id, 1);
assert_eq!(store.border_fill(2).unwrap().id, 2);
assert_eq!(store.border_fill(3).unwrap().id, 3);
}
#[test]
fn from_registry_injects_7_default_char_shapes() {
let registry: StyleRegistry = serde_json::from_str(
r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
)
.unwrap();
let store = HwpxStyleStore::from_registry(®istry);
assert_eq!(store.char_shape_count(), 7, "must have exactly 7 default charPr groups");
}
#[test]
fn from_registry_injects_20_default_para_shapes() {
let registry: StyleRegistry = serde_json::from_str(
r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
)
.unwrap();
let store = HwpxStyleStore::from_registry(®istry);
assert_eq!(store.para_shape_count(), 20, "must have exactly 20 default paraPr groups");
}
#[test]
fn default_char_shape_0_is_batang_10pt_black() {
let registry: StyleRegistry = serde_json::from_str(
r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
)
.unwrap();
let store = HwpxStyleStore::from_registry(®istry);
let cs = store.char_shape(CharShapeIndex::new(0)).unwrap();
assert_eq!(cs.height.as_i32(), 1000); assert_eq!(cs.text_color, Color::BLACK);
assert!(!cs.bold);
assert!(!cs.italic);
}
#[test]
fn default_char_shape_5_is_toc_heading() {
let registry: StyleRegistry = serde_json::from_str(
r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
)
.unwrap();
let store = HwpxStyleStore::from_registry(®istry);
let cs = store.char_shape(CharShapeIndex::new(5)).unwrap();
assert_eq!(cs.height.as_i32(), 1600); assert_eq!(cs.text_color, Color::from_rgb(0x2E, 0x74, 0xB5));
}
#[test]
fn from_registry_user_shapes_offset() {
let template = builtin_default().unwrap();
let registry = StyleRegistry::from_template(&template).unwrap();
let store = HwpxStyleStore::from_registry(®istry);
assert!(store.char_shape(CharShapeIndex::new(7)).is_ok());
assert!(store.para_shape(ParaShapeIndex::new(20)).is_ok());
}
#[test]
fn from_registry_default_style_refs_match_groups() {
let registry: StyleRegistry = serde_json::from_str(
r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
)
.unwrap();
let store = HwpxStyleStore::from_registry(®istry);
let defaults = HancomStyleSet::Modern.default_styles();
for (idx, entry) in defaults.iter().enumerate() {
let style = store.style(idx).unwrap();
assert_eq!(
style.char_pr_id_ref, entry.char_pr_group as u32,
"charPr ref mismatch for style '{}'",
entry.name
);
assert_eq!(
style.para_pr_id_ref, entry.para_pr_group as u32,
"paraPr ref mismatch for style '{}'",
entry.name
);
}
}
#[test]
fn char_shape_roundtrip_json() {
let cs = HwpxCharShape::default();
let json = serde_json::to_string(&cs).unwrap();
let restored: HwpxCharShape = serde_json::from_str(&json).unwrap();
assert_eq!(cs, restored);
}
#[test]
fn para_shape_roundtrip_json() {
let ps = HwpxParaShape::default();
let json = serde_json::to_string(&ps).unwrap();
let restored: HwpxParaShape = serde_json::from_str(&json).unwrap();
assert_eq!(ps, restored);
}
#[test]
fn style_store_roundtrip_json() {
let mut store = HwpxStyleStore::with_default_fonts("함초롬돋움");
store.push_char_shape(HwpxCharShape::default());
store.push_para_shape(HwpxParaShape::default());
let json = serde_json::to_string_pretty(&store).unwrap();
let restored: HwpxStyleStore = serde_json::from_str(&json).unwrap();
assert_eq!(store.font_count(), restored.font_count());
assert_eq!(store.char_shape_count(), restored.char_shape_count());
assert_eq!(store.para_shape_count(), restored.para_shape_count());
}
#[test]
fn from_registry_user_style_refs_are_offset_adjusted() {
let template = builtin_default().unwrap();
let registry = StyleRegistry::from_template(&template).unwrap();
let store = HwpxStyleStore::from_registry(®istry);
let defaults_len = HancomStyleSet::Modern.count();
for (i, (_, entry)) in registry.style_entries.iter().enumerate() {
let style = store.style(defaults_len + i).unwrap();
assert_eq!(
style.char_pr_id_ref,
(entry.char_shape_id.get() + 7) as u32,
"user charPr ref not offset-adjusted for style index {i}"
);
assert_eq!(
style.para_pr_id_ref,
(entry.para_shape_id.get() + 20) as u32,
"user paraPr ref not offset-adjusted for style index {i}"
);
}
}
#[test]
fn default_para_shape_0_is_batanggeul() {
let registry: StyleRegistry = serde_json::from_str(
r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
)
.unwrap();
let store = HwpxStyleStore::from_registry(®istry);
let ps = store.para_shape(ParaShapeIndex::new(0)).unwrap();
assert_eq!(ps.alignment, Alignment::Justify);
assert_eq!(ps.margin_left.as_i32(), 0);
assert_eq!(ps.line_spacing, 160);
}
#[test]
fn default_para_shape_2_is_outline1() {
let registry: StyleRegistry = serde_json::from_str(
r#"{"fonts":[],"char_shapes":[],"para_shapes":[],"style_entries":{}}"#,
)
.unwrap();
let store = HwpxStyleStore::from_registry(®istry);
let ps = store.para_shape(ParaShapeIndex::new(2)).unwrap();
assert_eq!(ps.alignment, Alignment::Justify);
assert_eq!(ps.margin_left.as_i32(), 1000);
assert_eq!(ps.line_spacing, 160);
}
}