use serde::Serialize;
use hwpforge_smithy_hwpx::presets::builtin_presets;
use hwpforge_smithy_hwpx::{HwpxDecoder, HwpxEncoder};
use crate::output::{read_file_bytes, write_output_file, ToolErrorInfo};
#[derive(Debug, Serialize)]
pub struct RestyleData {
pub output_path: String,
pub applied_preset: String,
pub size_bytes: u64,
pub sections: usize,
}
pub fn run_restyle(
file_path: &str,
preset: &str,
output_path: &str,
) -> Result<RestyleData, ToolErrorInfo> {
if !output_path.ends_with(".hwpx") {
return Err(ToolErrorInfo::new(
"INVALID_EXTENSION",
format!("Output path must end with .hwpx: {output_path}"),
"Use a .hwpx extension for the output file.",
));
}
let presets = builtin_presets();
let preset_info = presets.iter().find(|p| p.name == preset).ok_or_else(|| {
ToolErrorInfo::new(
"PRESET_NOT_FOUND",
format!("Preset '{preset}' not found"),
"Use hwpforge_templates to see available presets.",
)
})?;
let preset_font = preset_info.font.clone();
let bytes = read_file_bytes(file_path)?;
let hwpx_doc = HwpxDecoder::decode(&bytes).map_err(|e| {
ToolErrorInfo::new(
"DECODE_ERROR",
format!("HWPX decode failed: {e}"),
"Check that the file is a valid HWPX document.",
)
})?;
let mut style_store = hwpx_doc.style_store;
let original_base: Option<String> =
style_store.iter_fonts().next().map(|f| f.face_name.clone());
match original_base {
Some(ref base) => style_store.replace_font(base, &preset_font),
None => {
return Err(ToolErrorInfo::new(
"NO_FONTS",
"Document has no fonts to restyle",
"The HWPX file may be malformed. Use hwpforge_validate to check.",
));
}
}
let validated = hwpx_doc.document.validate().map_err(|e| {
ToolErrorInfo::new(
"VALIDATION_ERROR",
format!("Document validation failed: {e}"),
"Check document structure.",
)
})?;
let section_count = validated.section_count();
let output_bytes = HwpxEncoder::encode(&validated, &style_store, &hwpx_doc.image_store)
.map_err(|e| {
ToolErrorInfo::new(
"ENCODE_ERROR",
format!("HWPX encoding failed: {e}"),
"This may be a bug. Please report at https://github.com/ai-screams/HwpForge/issues",
)
})?;
write_output_file(output_path, &output_bytes)?;
let size_bytes = output_bytes.len() as u64;
Ok(RestyleData {
output_path: output_path.to_string(),
applied_preset: preset.to_string(),
size_bytes,
sections: section_count,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn restyle_invalid_extension() {
let err = run_restyle("/tmp/doc.hwpx", "default", "/tmp/out.txt").unwrap_err();
assert_eq!(err.code, "INVALID_EXTENSION");
}
#[test]
fn restyle_unknown_preset() {
let err = run_restyle("/tmp/doc.hwpx", "unknown", "/tmp/out.hwpx").unwrap_err();
assert_eq!(err.code, "PRESET_NOT_FOUND");
}
#[test]
fn restyle_missing_file() {
let err = run_restyle("/nonexistent/file.hwpx", "modern", "/tmp/out.hwpx").unwrap_err();
assert_eq!(err.code, "FILE_NOT_FOUND");
}
#[test]
fn restyle_happy_path() {
let dir = tempfile::tempdir().unwrap();
let hwpx_path = dir.path().join("source.hwpx");
crate::tools::convert::run_convert(
"# Test\n\nSome content.",
false,
hwpx_path.to_str().unwrap(),
"default",
)
.unwrap();
let out_path = dir.path().join("restyled.hwpx");
let data =
run_restyle(hwpx_path.to_str().unwrap(), "modern", out_path.to_str().unwrap()).unwrap();
assert!(out_path.exists());
assert_eq!(data.applied_preset, "modern");
assert!(data.size_bytes > 0);
assert!(data.sections >= 1);
}
#[test]
fn restyle_preserves_all_shape_indices_with_complex_doc() {
let dir = tempfile::tempdir().unwrap();
let hwpx_path = dir.path().join("complex.hwpx");
let md = "# Heading\n\nBody text.\n\n```rust\nfn main() {}\n```\n\n> Blockquote\n";
crate::tools::convert::run_convert(md, false, hwpx_path.to_str().unwrap(), "default")
.unwrap();
let out_path = dir.path().join("restyled.hwpx");
let data = run_restyle(hwpx_path.to_str().unwrap(), "classic", out_path.to_str().unwrap())
.unwrap();
assert!(out_path.exists());
assert_eq!(data.applied_preset, "classic");
assert!(data.size_bytes > 0);
let restyled_bytes = std::fs::read(&out_path).unwrap();
let restyled_doc = HwpxDecoder::decode(&restyled_bytes).unwrap();
assert!(!restyled_doc.document.sections().is_empty());
}
}