use crate::error::HwpxError;
use crate::{ExportedSection, HwpxDecoder, HwpxPatcher, PackageReader};
#[derive(Debug)]
pub struct SectionExportOutcome {
pub exported: ExportedSection,
pub warning: Option<SectionWorkflowWarning>,
}
#[derive(Debug)]
pub struct SectionPatchOutcome {
pub bytes: Vec<u8>,
pub patched_section: usize,
pub sections: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SectionWorkflowWarning {
PreservationMetadataUnavailable {
detail: String,
},
}
impl SectionWorkflowWarning {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::PreservationMetadataUnavailable { .. } => "PRESERVATION_METADATA_UNAVAILABLE",
}
}
#[must_use]
pub fn message(&self) -> String {
match self {
Self::PreservationMetadataUnavailable { detail } => {
format!("Preserving patch metadata unavailable: {detail}")
}
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SectionWorkflowError {
#[error("HWPX decode failed: {detail}")]
Decode {
detail: String,
},
#[error("Section {requested} does not exist (document has {sections} sections)")]
SectionOutOfRange {
requested: usize,
sections: usize,
},
#[error("Requested section {requested} but JSON contains section {actual} data")]
SectionIndexMismatch {
requested: usize,
actual: usize,
},
#[error("Preserving patch failed: {0}")]
PreservingPatch(#[from] HwpxError),
}
impl HwpxPatcher {
pub fn export_section_for_edit(
base_bytes: &[u8],
section_idx: usize,
include_styles: bool,
) -> Result<SectionExportOutcome, SectionWorkflowError> {
let hwpx_doc = HwpxDecoder::decode(base_bytes)
.map_err(|error| SectionWorkflowError::Decode { detail: error.to_string() })?;
let section = hwpx_doc.document.sections().get(section_idx).cloned().ok_or(
SectionWorkflowError::SectionOutOfRange {
requested: section_idx,
sections: hwpx_doc.document.sections().len(),
},
)?;
let preservation =
match HwpxPatcher::export_section_preservation(base_bytes, section_idx, §ion) {
Ok(metadata) => (Some(metadata), None),
Err(error) => (
None,
Some(SectionWorkflowWarning::PreservationMetadataUnavailable {
detail: error.to_string(),
}),
),
};
let exported = ExportedSection {
section_index: section_idx,
section,
styles: include_styles.then_some(hwpx_doc.style_store),
preservation: preservation.0,
};
Ok(SectionExportOutcome { exported, warning: preservation.1 })
}
pub fn patch_exported_section(
base_bytes: &[u8],
section_idx: usize,
exported: &ExportedSection,
) -> Result<SectionPatchOutcome, SectionWorkflowError> {
let section_count = PackageReader::new(base_bytes)
.map_err(|error| SectionWorkflowError::Decode { detail: error.to_string() })?
.section_count();
if exported.section_index != section_idx {
return Err(SectionWorkflowError::SectionIndexMismatch {
requested: section_idx,
actual: exported.section_index,
});
}
if section_idx >= section_count {
return Err(SectionWorkflowError::SectionOutOfRange {
requested: section_idx,
sections: section_count,
});
}
let bytes = HwpxPatcher::patch_section_preserving(
base_bytes,
section_idx,
&exported.section,
exported.styles.as_ref(),
exported.preservation.as_ref(),
)?;
Ok(SectionPatchOutcome { bytes, patched_section: section_idx, sections: section_count })
}
}
#[cfg(test)]
mod tests {
use super::*;
use hwpforge_core::document::Document;
use hwpforge_core::image::ImageStore;
use hwpforge_core::page::PageSettings;
use hwpforge_core::paragraph::Paragraph;
use hwpforge_core::run::Run;
use hwpforge_core::section::Section;
use hwpforge_core::Draft;
use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
use crate::{HwpxCharShape, HwpxEncoder, HwpxParaShape, HwpxResult, HwpxStyleStore};
fn minimal_hwpx_bytes() -> HwpxResult<Vec<u8>> {
let mut doc: Document<Draft> = Document::new();
doc.add_section(Section::with_paragraphs(
vec![Paragraph::with_runs(
vec![Run::text("hello", CharShapeIndex::new(0))],
ParaShapeIndex::new(0),
)],
PageSettings::default(),
));
let mut styles: HwpxStyleStore = HwpxStyleStore::with_default_fonts("함초롬돋움");
styles.push_char_shape(HwpxCharShape::default());
styles.push_para_shape(HwpxParaShape::default());
let validated = doc.validate()?;
HwpxEncoder::encode(&validated, &styles, &ImageStore::new())
}
#[test]
fn export_section_for_edit_embeds_preservation_when_available() {
let bytes = minimal_hwpx_bytes().unwrap();
let outcome = HwpxPatcher::export_section_for_edit(&bytes, 0, true).unwrap();
assert!(outcome.warning.is_none());
assert_eq!(outcome.exported.section_index, 0);
assert!(outcome.exported.styles.is_some());
assert!(outcome.exported.preservation.is_some());
}
#[test]
fn patch_exported_section_rejects_index_mismatch() {
let bytes = minimal_hwpx_bytes().unwrap();
let mut exported = HwpxPatcher::export_section_for_edit(&bytes, 0, true).unwrap().exported;
exported.section_index = 1;
let error = HwpxPatcher::patch_exported_section(&bytes, 0, &exported).unwrap_err();
assert!(matches!(
error,
SectionWorkflowError::SectionIndexMismatch { requested: 0, actual: 1 }
));
}
#[test]
fn patch_exported_section_prefers_index_mismatch_over_out_of_range() {
let bytes = minimal_hwpx_bytes().unwrap();
let exported = HwpxPatcher::export_section_for_edit(&bytes, 0, true).unwrap().exported;
let error = HwpxPatcher::patch_exported_section(&bytes, 99, &exported).unwrap_err();
assert!(matches!(
error,
SectionWorkflowError::SectionIndexMismatch { requested: 99, actual: 0 }
));
}
}