1use crate::error::HwpxError;
8use crate::{ExportedSection, HwpxDecoder, HwpxPatcher, PackageReader};
9
10#[derive(Debug)]
12pub struct SectionExportOutcome {
13 pub exported: ExportedSection,
15 pub warning: Option<SectionWorkflowWarning>,
17}
18
19#[derive(Debug)]
21pub struct SectionPatchOutcome {
22 pub bytes: Vec<u8>,
24 pub patched_section: usize,
26 pub sections: usize,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32#[non_exhaustive]
33pub enum SectionWorkflowWarning {
34 PreservationMetadataUnavailable {
37 detail: String,
39 },
40}
41
42impl SectionWorkflowWarning {
43 #[must_use]
45 pub fn code(&self) -> &'static str {
46 match self {
47 Self::PreservationMetadataUnavailable { .. } => "PRESERVATION_METADATA_UNAVAILABLE",
48 }
49 }
50
51 #[must_use]
53 pub fn message(&self) -> String {
54 match self {
55 Self::PreservationMetadataUnavailable { detail } => {
56 format!("Preserving patch metadata unavailable: {detail}")
57 }
58 }
59 }
60}
61
62#[derive(Debug, thiserror::Error)]
64#[non_exhaustive]
65pub enum SectionWorkflowError {
66 #[error("HWPX decode failed: {detail}")]
68 Decode {
69 detail: String,
71 },
72 #[error("Section {requested} does not exist (document has {sections} sections)")]
74 SectionOutOfRange {
75 requested: usize,
77 sections: usize,
79 },
80 #[error("Requested section {requested} but JSON contains section {actual} data")]
82 SectionIndexMismatch {
83 requested: usize,
85 actual: usize,
87 },
88 #[error("Preserving patch failed: {0}")]
90 PreservingPatch(#[from] HwpxError),
91}
92
93impl HwpxPatcher {
94 pub fn export_section_for_edit(
96 base_bytes: &[u8],
97 section_idx: usize,
98 include_styles: bool,
99 ) -> Result<SectionExportOutcome, SectionWorkflowError> {
100 let hwpx_doc = HwpxDecoder::decode(base_bytes)
101 .map_err(|error| SectionWorkflowError::Decode { detail: error.to_string() })?;
102
103 let section = hwpx_doc.document.sections().get(section_idx).cloned().ok_or(
104 SectionWorkflowError::SectionOutOfRange {
105 requested: section_idx,
106 sections: hwpx_doc.document.sections().len(),
107 },
108 )?;
109
110 let preservation =
111 match HwpxPatcher::export_section_preservation(base_bytes, section_idx, §ion) {
112 Ok(metadata) => (Some(metadata), None),
113 Err(error) => (
114 None,
115 Some(SectionWorkflowWarning::PreservationMetadataUnavailable {
116 detail: error.to_string(),
117 }),
118 ),
119 };
120
121 let exported = ExportedSection {
122 section_index: section_idx,
123 section,
124 styles: include_styles.then_some(hwpx_doc.style_store),
125 preservation: preservation.0,
126 };
127
128 Ok(SectionExportOutcome { exported, warning: preservation.1 })
129 }
130
131 pub fn patch_exported_section(
133 base_bytes: &[u8],
134 section_idx: usize,
135 exported: &ExportedSection,
136 ) -> Result<SectionPatchOutcome, SectionWorkflowError> {
137 let section_count = PackageReader::new(base_bytes)
138 .map_err(|error| SectionWorkflowError::Decode { detail: error.to_string() })?
139 .section_count();
140
141 if exported.section_index != section_idx {
142 return Err(SectionWorkflowError::SectionIndexMismatch {
143 requested: section_idx,
144 actual: exported.section_index,
145 });
146 }
147
148 if section_idx >= section_count {
149 return Err(SectionWorkflowError::SectionOutOfRange {
150 requested: section_idx,
151 sections: section_count,
152 });
153 }
154
155 let bytes = HwpxPatcher::patch_section_preserving(
156 base_bytes,
157 section_idx,
158 &exported.section,
159 exported.styles.as_ref(),
160 exported.preservation.as_ref(),
161 )?;
162
163 Ok(SectionPatchOutcome { bytes, patched_section: section_idx, sections: section_count })
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use hwpforge_core::document::Document;
171 use hwpforge_core::image::ImageStore;
172 use hwpforge_core::page::PageSettings;
173 use hwpforge_core::paragraph::Paragraph;
174 use hwpforge_core::run::Run;
175 use hwpforge_core::section::Section;
176 use hwpforge_core::Draft;
177 use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
178
179 use crate::{HwpxCharShape, HwpxEncoder, HwpxParaShape, HwpxResult, HwpxStyleStore};
180
181 fn minimal_hwpx_bytes() -> HwpxResult<Vec<u8>> {
182 let mut doc: Document<Draft> = Document::new();
183 doc.add_section(Section::with_paragraphs(
184 vec![Paragraph::with_runs(
185 vec![Run::text("hello", CharShapeIndex::new(0))],
186 ParaShapeIndex::new(0),
187 )],
188 PageSettings::default(),
189 ));
190 let mut styles: HwpxStyleStore = HwpxStyleStore::with_default_fonts("함초롬돋움");
191 styles.push_char_shape(HwpxCharShape::default());
192 styles.push_para_shape(HwpxParaShape::default());
193 let validated = doc.validate()?;
194 HwpxEncoder::encode(&validated, &styles, &ImageStore::new())
195 }
196
197 #[test]
198 fn export_section_for_edit_embeds_preservation_when_available() {
199 let bytes = minimal_hwpx_bytes().unwrap();
200 let outcome = HwpxPatcher::export_section_for_edit(&bytes, 0, true).unwrap();
201 assert!(outcome.warning.is_none());
202 assert_eq!(outcome.exported.section_index, 0);
203 assert!(outcome.exported.styles.is_some());
204 assert!(outcome.exported.preservation.is_some());
205 }
206
207 #[test]
208 fn patch_exported_section_rejects_index_mismatch() {
209 let bytes = minimal_hwpx_bytes().unwrap();
210 let mut exported = HwpxPatcher::export_section_for_edit(&bytes, 0, true).unwrap().exported;
211 exported.section_index = 1;
212
213 let error = HwpxPatcher::patch_exported_section(&bytes, 0, &exported).unwrap_err();
214 assert!(matches!(
215 error,
216 SectionWorkflowError::SectionIndexMismatch { requested: 0, actual: 1 }
217 ));
218 }
219
220 #[test]
221 fn patch_exported_section_prefers_index_mismatch_over_out_of_range() {
222 let bytes = minimal_hwpx_bytes().unwrap();
223 let exported = HwpxPatcher::export_section_for_edit(&bytes, 0, true).unwrap().exported;
224
225 let error = HwpxPatcher::patch_exported_section(&bytes, 99, &exported).unwrap_err();
226 assert!(matches!(
227 error,
228 SectionWorkflowError::SectionIndexMismatch { requested: 99, actual: 0 }
229 ));
230 }
231}