1use schemars::JsonSchema;
18use serde::{Deserialize, Serialize};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
25pub struct LineSeg {
26 pub textpos: u32,
28 pub vertpos: i32,
30 pub vertsize: i32,
32 pub textheight: i32,
34 pub baseline: i32,
36 pub spacing: i32,
38 pub horzpos: i32,
40 pub horzsize: i32,
42 pub flags: u32,
44}
45
46#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
51pub struct LayoutCache {
52 pub lines: Vec<LineSeg>,
54}
55
56impl LayoutCache {
57 pub fn new(lines: Vec<LineSeg>) -> Self {
59 Self { lines }
60 }
61
62 pub fn line_count(&self) -> usize {
64 self.lines.len()
65 }
66
67 pub fn is_empty(&self) -> bool {
69 self.lines.is_empty()
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 fn seg(textpos: u32, vertpos: i32) -> LineSeg {
78 LineSeg {
79 textpos,
80 vertpos,
81 vertsize: 1000,
82 textheight: 1000,
83 baseline: 850,
84 spacing: 600,
85 horzpos: 0,
86 horzsize: 48188,
87 flags: 0x0060_0000,
88 }
89 }
90
91 #[test]
92 fn lineseg_boundary_values_roundtrip_serde() {
93 let s = LineSeg {
94 textpos: u32::MAX,
95 vertpos: i32::MIN,
96 vertsize: i32::MAX,
97 textheight: 0,
98 baseline: -1,
99 spacing: i32::MIN,
100 horzpos: i32::MAX,
101 horzsize: 0,
102 flags: u32::MAX,
103 };
104 let json = serde_json::to_string(&s).unwrap();
105 let back: LineSeg = serde_json::from_str(&json).unwrap();
106 assert_eq!(s, back);
107 }
108
109 #[test]
110 fn layout_cache_default_is_empty() {
111 let c = LayoutCache::default();
112 assert!(c.is_empty());
113 assert_eq!(c.line_count(), 0);
114 }
115
116 #[test]
117 fn layout_cache_preserves_wire_order() {
118 let c = LayoutCache::new(vec![seg(0, 0), seg(70, 1600), seg(122, 3200)]);
119 assert_eq!(c.line_count(), 3);
120 assert_eq!(c.lines[1].textpos, 70);
121 assert_eq!(c.lines[2].vertpos, 3200);
122 }
123
124 #[test]
125 fn layout_cache_eq_is_structural() {
126 let a = LayoutCache::new(vec![seg(0, 0)]);
127 let b = LayoutCache::new(vec![seg(0, 0)]);
128 let c = LayoutCache::new(vec![seg(0, 16)]);
129 assert_eq!(a, b);
130 assert_ne!(a, c);
131 }
132
133 #[test]
134 fn paragraph_json_without_cache_field_deserializes() {
135 let old_json = r#"{"runs":[],"para_shape_id":0}"#;
137 let p: crate::paragraph::Paragraph = serde_json::from_str(old_json).unwrap();
138 assert!(p.layout_cache.is_none());
139 }
140
141 #[test]
142 fn paragraph_json_omits_cache_when_none() {
143 let p = crate::paragraph::Paragraph::new(hwpforge_foundation::ParaShapeIndex::new(0));
144 let json = serde_json::to_string(&p).unwrap();
145 assert!(!json.contains("layout_cache"), "None 캐시는 직렬화 생략: {json}");
146 let mut cached = p.clone();
147 cached.layout_cache = Some(LayoutCache::new(vec![seg(0, 0)]));
148 let json2 = serde_json::to_string(&cached).unwrap();
149 assert!(json2.contains("layout_cache"));
150 let back: crate::paragraph::Paragraph = serde_json::from_str(&json2).unwrap();
151 assert_eq!(cached, back);
152 }
153
154 mod traversal {
159 use super::*;
160 use crate::caption::{Caption, CaptionSide};
161 use crate::control::Control;
162 use crate::document::Document;
163 use crate::page::PageSettings;
164 use crate::paragraph::Paragraph;
165 use crate::run::Run;
166 use crate::section::{HeaderFooter, MasterPage, Section};
167 use crate::table::{Table, TableCell, TableRow};
168 use hwpforge_foundation::{ApplyPageType, CharShapeIndex, HwpUnit, ParaShapeIndex};
169
170 fn cached_para(text: &str) -> Paragraph {
172 let mut p = Paragraph::with_runs(
173 vec![Run::text(text, CharShapeIndex::new(0))],
174 ParaShapeIndex::new(0),
175 );
176 p.layout_cache = Some(LayoutCache::new(vec![seg(0, 0)]));
177 p
178 }
179
180 fn one_cell_table(cell_para: Paragraph) -> Table {
181 Table::new(vec![TableRow::new(vec![TableCell::new(
184 vec![cell_para],
185 HwpUnit::from_pt(100.0).unwrap(),
186 )])])
187 .with_layout_cache(crate::table::TableLayoutCache::new(
188 Some(HwpUnit::from_pt(56.7).unwrap()),
189 true,
190 ))
191 .with_out_margin(crate::table::TableMargin::default())
192 }
193
194 fn document_with_all_containers() -> (Document<crate::document::Draft>, usize) {
198 let mut expected = 0usize;
199
200 let inner = one_cell_table(cached_para("inner-cell"));
202 let mut mid_cell = cached_para("mid-cell");
203 mid_cell.add_run(Run::table(inner, CharShapeIndex::new(0)));
204 let outer = one_cell_table(mid_cell)
205 .with_caption(Caption::new(vec![cached_para("tbl-caption")], CaptionSide::Bottom));
206 let mut host = cached_para("tbl-host");
207 host.add_run(Run::table(outer, CharShapeIndex::new(0)));
208 expected += 4; let mut textbox = Control::text_box(
212 vec![cached_para("textbox-body")],
213 HwpUnit::from_pt(100.0).unwrap(),
214 HwpUnit::from_pt(50.0).unwrap(),
215 );
216 if let Control::TextBox { caption, .. } = &mut textbox {
217 *caption =
218 Some(Caption::new(vec![cached_para("textbox-caption")], CaptionSide::Top));
219 }
220 let mut tb_host = cached_para("textbox-host");
221 tb_host.add_run(Run::control(textbox, CharShapeIndex::new(0)));
222 expected += 3; let child = Control::text_box(
226 vec![cached_para("group-child-body")],
227 HwpUnit::from_pt(10.0).unwrap(),
228 HwpUnit::from_pt(10.0).unwrap(),
229 );
230 let group = Control::Group {
231 children: vec![child],
232 width: HwpUnit::from_pt(10.0).unwrap(),
233 height: HwpUnit::from_pt(10.0).unwrap(),
234 horz_offset: 0,
235 vert_offset: 0,
236 inst_id: None,
237 };
238 let mut group_host = cached_para("group-host");
239 group_host.add_run(Run::control(group, CharShapeIndex::new(0)));
240 expected += 2; let memo = Control::memo_with_anchor(
244 vec![cached_para("memo-body")],
245 vec![Run::table(
246 one_cell_table(cached_para("memo-anchor-cell")),
247 CharShapeIndex::new(0),
248 )],
249 );
250 let mut memo_host = cached_para("memo-host");
251 memo_host.add_run(Run::control(memo, CharShapeIndex::new(0)));
252 expected += 3; let mut note_host = cached_para("note-host");
256 note_host.add_run(Run::control(
257 Control::footnote(vec![cached_para("footnote-body")]),
258 CharShapeIndex::new(0),
259 ));
260 note_host.add_run(Run::control(
261 Control::ellipse_with_text(
262 HwpUnit::from_pt(20.0).unwrap(),
263 HwpUnit::from_pt(20.0).unwrap(),
264 vec![cached_para("ellipse-body")],
265 ),
266 CharShapeIndex::new(0),
267 ));
268 expected += 3; let mut section = Section::with_paragraphs(
271 vec![host, tb_host, group_host, memo_host, note_host],
272 PageSettings::a4(),
273 );
274 section.headers.push(HeaderFooter::all_pages(vec![cached_para("header")]));
275 section.footers.push(HeaderFooter::all_pages(vec![cached_para("footer")]));
276 section.master_pages =
277 Some(vec![MasterPage::new(ApplyPageType::Both, vec![cached_para("master")])]);
278 expected += 3; let mut doc = Document::new();
281 doc.add_section(section);
282 (doc, expected)
283 }
284
285 #[test]
286 fn for_each_paragraph_mut_visits_every_container() {
287 let (mut doc, expected) = document_with_all_containers();
288 let mut visited = Vec::new();
289 doc.for_each_paragraph_mut(|p| visited.push(p.text_content()));
290 assert_eq!(visited.len(), expected, "visited: {visited:?}");
291 for needle in [
293 "inner-cell",
294 "tbl-caption",
295 "textbox-caption",
296 "group-child-body",
297 "memo-anchor-cell",
298 "footnote-body",
299 "master",
300 ] {
301 assert!(visited.iter().any(|t| t == needle), "missing {needle}: {visited:?}");
302 }
303 }
304
305 #[test]
306 fn strip_layout_caches_clears_every_container() {
307 let (mut doc, expected) = document_with_all_containers();
308 doc.strip_layout_caches();
309 let mut remaining = 0;
310 let mut total = 0;
311 let mut table_caches = 0;
312 let mut table_margins = 0;
313 doc.for_each_paragraph_mut(|p| {
314 total += 1;
315 if p.layout_cache.is_some() {
316 remaining += 1;
317 }
318 for run in &p.runs {
319 if let crate::run::RunContent::Table(t) = &run.content {
320 table_caches += usize::from(t.layout_cache.is_some());
321 table_margins += usize::from(t.out_margin.is_some());
322 }
323 }
324 });
325 assert_eq!(total, expected);
326 assert_eq!(remaining, 0);
327 assert_eq!(table_caches, 0, "table layout caches must be stripped");
329 assert!(table_margins >= 2, "structural margins must survive strip");
331 }
332
333 #[test]
334 fn cache_difference_breaks_eq_until_stripped() {
335 let (doc_a, _) = document_with_all_containers();
336 let (mut doc_b, _) = document_with_all_containers();
337 assert_eq!(doc_a, doc_b);
338 doc_b.strip_layout_caches();
339 assert_ne!(doc_a, doc_b, "derived eq must still see cache differences");
340 let mut doc_a2 = doc_a.clone();
341 doc_a2.strip_layout_caches();
342 assert_eq!(doc_a2, doc_b, "normalized copies must compare equal");
343 }
344 }
345}