use crate::error::Hwp2MdError;
use serde::Deserialize;
use std::path::Path;
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default)]
pub struct StyleTemplate {
pub page: PageStyle,
pub font: FontStyle,
pub heading: HeadingStyle,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default)]
pub struct PageStyle {
pub width: Option<u32>,
pub height: Option<u32>,
pub landscape: Option<bool>,
pub margin: MarginStyle,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default)]
pub struct MarginStyle {
pub left: Option<u32>,
pub right: Option<u32>,
pub top: Option<u32>,
pub bottom: Option<u32>,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default)]
pub struct FontStyle {
pub default: Option<String>,
pub code: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(default)]
pub struct HeadingStyle {
pub line_spacing: Option<u32>,
}
impl StyleTemplate {
pub fn from_file(path: &Path) -> Result<Self, Hwp2MdError> {
let content = std::fs::read_to_string(path).map_err(|e| {
Hwp2MdError::StyleLoad(format!("failed to read style file {}: {e}", path.display()))
})?;
Self::from_yaml(&content)
}
pub fn from_yaml(yaml: &str) -> Result<Self, Hwp2MdError> {
let t: Self = serde_yml::from_str(yaml)
.map_err(|e| Hwp2MdError::StyleLoad(format!("invalid style YAML: {e}")))?;
t.validate()?;
Ok(t)
}
fn validate(&self) -> Result<(), Hwp2MdError> {
if let Some(w) = self.page.width {
if w == 0 {
return Err(Hwp2MdError::StyleLoad("page.width must be > 0".into()));
}
}
if let Some(h) = self.page.height {
if h == 0 {
return Err(Hwp2MdError::StyleLoad("page.height must be > 0".into()));
}
}
if let Some(ls) = self.heading.line_spacing {
if ls == 0 {
return Err(Hwp2MdError::StyleLoad(
"heading.line_spacing must be > 0".into(),
));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_yaml_produces_defaults() {
let t = StyleTemplate::from_yaml("{}").unwrap();
assert!(t.page.width.is_none());
assert!(t.font.default.is_none());
assert!(t.heading.line_spacing.is_none());
}
#[test]
fn partial_yaml_parsed_correctly() {
let yaml = r#"
page:
width: 59528
margin:
left: 8000
font:
default: "맑은 고딕"
"#;
let t = StyleTemplate::from_yaml(yaml).unwrap();
assert_eq!(t.page.width, Some(59528));
assert!(t.page.height.is_none());
assert_eq!(t.page.margin.left, Some(8000));
assert!(t.page.margin.right.is_none());
assert_eq!(t.font.default.as_deref(), Some("맑은 고딕"));
}
#[test]
fn full_yaml_parsed() {
let yaml = r#"
page:
width: 59528
height: 84188
landscape: true
margin:
left: 5670
right: 5670
top: 4252
bottom: 4252
font:
default: "바탕"
code: "D2Coding"
heading:
line_spacing: 200
"#;
let t = StyleTemplate::from_yaml(yaml).unwrap();
assert_eq!(t.page.width, Some(59528));
assert_eq!(t.page.height, Some(84188));
assert_eq!(t.page.landscape, Some(true));
assert_eq!(t.page.margin.left, Some(5670));
assert_eq!(t.page.margin.right, Some(5670));
assert_eq!(t.page.margin.top, Some(4252));
assert_eq!(t.page.margin.bottom, Some(4252));
assert_eq!(t.font.default.as_deref(), Some("바탕"));
assert_eq!(t.font.code.as_deref(), Some("D2Coding"));
assert_eq!(t.heading.line_spacing, Some(200));
}
#[test]
fn invalid_yaml_returns_error() {
let result = StyleTemplate::from_yaml("not: [valid: yaml:");
assert!(result.is_err());
}
#[test]
fn missing_file_returns_error() {
let result = StyleTemplate::from_file(Path::new("/nonexistent/style.yaml"));
assert!(result.is_err());
}
#[test]
fn zero_width_rejected() {
let result = StyleTemplate::from_yaml("page:\n width: 0");
assert!(result.is_err());
}
#[test]
fn zero_height_rejected() {
let result = StyleTemplate::from_yaml("page:\n height: 0");
assert!(result.is_err());
}
#[test]
fn zero_line_spacing_rejected() {
let result = StyleTemplate::from_yaml("heading:\n line_spacing: 0");
assert!(result.is_err());
}
}