1use hwpforge_foundation::CharShapeIndex;
30use schemars::JsonSchema;
31use serde::{Deserialize, Serialize};
32
33use crate::control::Control;
34use crate::image::Image;
35use crate::table::Table;
36
37#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
53pub struct Run {
54 pub content: RunContent,
56 pub char_shape_id: CharShapeIndex,
58}
59
60impl Run {
61 pub fn text(s: impl Into<String>, char_shape_id: CharShapeIndex) -> Self {
76 Self { content: RunContent::Text(s.into()), char_shape_id }
77 }
78
79 pub fn table(table: Table, char_shape_id: CharShapeIndex) -> Self {
93 Self { content: RunContent::Table(Box::new(table)), char_shape_id }
94 }
95
96 pub fn image(image: Image, char_shape_id: CharShapeIndex) -> Self {
110 Self { content: RunContent::Image(image), char_shape_id }
111 }
112
113 pub fn control(control: Control, char_shape_id: CharShapeIndex) -> Self {
130 Self { content: RunContent::Control(Box::new(control)), char_shape_id }
131 }
132}
133
134impl std::fmt::Display for Run {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 write!(f, "Run({})", self.content)
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
161#[non_exhaustive]
162pub enum RunContent {
163 Text(String),
165 Table(Box<Table>),
167 Image(Image),
169 Control(Box<Control>),
171}
172
173impl RunContent {
174 pub fn as_text(&self) -> Option<&str> {
188 match self {
189 Self::Text(s) => Some(s),
190 _ => None,
191 }
192 }
193
194 pub fn as_table(&self) -> Option<&Table> {
196 match self {
197 Self::Table(t) => Some(t),
198 _ => None,
199 }
200 }
201
202 pub fn as_image(&self) -> Option<&Image> {
204 match self {
205 Self::Image(i) => Some(i),
206 _ => None,
207 }
208 }
209
210 pub fn as_control(&self) -> Option<&Control> {
212 match self {
213 Self::Control(c) => Some(c),
214 _ => None,
215 }
216 }
217
218 pub fn is_text(&self) -> bool {
220 matches!(self, Self::Text(_))
221 }
222
223 pub fn is_table(&self) -> bool {
225 matches!(self, Self::Table(_))
226 }
227
228 pub fn is_image(&self) -> bool {
230 matches!(self, Self::Image(_))
231 }
232
233 pub fn is_control(&self) -> bool {
235 matches!(self, Self::Control(_))
236 }
237}
238
239impl std::fmt::Display for RunContent {
240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 match self {
242 Self::Text(s) => {
243 if s.len() <= 50 {
244 write!(f, "Text(\"{s}\")")
245 } else {
246 let truncated: String = s.chars().take(50).collect();
247 write!(f, "Text(\"{truncated}...\")")
248 }
249 }
250 Self::Table(t) => write!(f, "{t}"),
251 Self::Image(i) => write!(f, "{i}"),
252 Self::Control(c) => write!(f, "{c}"),
253 }
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260 use crate::image::ImageFormat;
261 use hwpforge_foundation::HwpUnit;
262
263 #[test]
264 fn run_text_constructor() {
265 let run = Run::text("Hello", CharShapeIndex::new(0));
266 assert_eq!(run.content.as_text(), Some("Hello"));
267 assert_eq!(run.char_shape_id, CharShapeIndex::new(0));
268 }
269
270 #[test]
271 fn run_text_from_string() {
272 let s = String::from("owned");
273 let run = Run::text(s, CharShapeIndex::new(1));
274 assert_eq!(run.content.as_text(), Some("owned"));
275 }
276
277 #[test]
278 fn run_table_constructor() {
279 let table = Table::new(vec![]);
280 let run = Run::table(table, CharShapeIndex::new(0));
281 assert!(run.content.is_table());
282 assert!(run.content.as_table().unwrap().is_empty());
283 }
284
285 #[test]
286 fn run_image_constructor() {
287 let img = Image::new("test.png", HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Png);
288 let run = Run::image(img, CharShapeIndex::new(0));
289 assert!(run.content.is_image());
290 assert_eq!(run.content.as_image().unwrap().path, "test.png");
291 }
292
293 #[test]
294 fn run_control_constructor() {
295 let ctrl =
296 Control::Hyperlink { text: "link".to_string(), url: "https://example.com".to_string() };
297 let run = Run::control(ctrl, CharShapeIndex::new(0));
298 assert!(run.content.is_control());
299 assert!(run.content.as_control().unwrap().is_hyperlink());
300 }
301
302 #[test]
305 fn run_content_text_checks() {
306 let c = RunContent::Text("hi".to_string());
307 assert!(c.is_text());
308 assert!(!c.is_table());
309 assert!(!c.is_image());
310 assert!(!c.is_control());
311 }
312
313 #[test]
314 fn run_content_table_checks() {
315 let c = RunContent::Table(Box::new(Table::new(vec![])));
316 assert!(!c.is_text());
317 assert!(c.is_table());
318 }
319
320 #[test]
321 fn run_content_image_checks() {
322 let c =
323 RunContent::Image(Image::new("x.png", HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Png));
324 assert!(!c.is_text());
325 assert!(c.is_image());
326 }
327
328 #[test]
329 fn run_content_control_checks() {
330 let c =
331 RunContent::Control(Box::new(Control::Unknown { tag: "x".to_string(), data: None }));
332 assert!(!c.is_text());
333 assert!(c.is_control());
334 }
335
336 #[test]
339 fn as_text_returns_none_for_non_text() {
340 let c = RunContent::Table(Box::new(Table::new(vec![])));
341 assert!(c.as_text().is_none());
342 }
343
344 #[test]
345 fn as_table_returns_none_for_non_table() {
346 let c = RunContent::Text("hi".to_string());
347 assert!(c.as_table().is_none());
348 }
349
350 #[test]
351 fn as_image_returns_none_for_non_image() {
352 let c = RunContent::Text("hi".to_string());
353 assert!(c.as_image().is_none());
354 }
355
356 #[test]
357 fn as_control_returns_none_for_non_control() {
358 let c = RunContent::Text("hi".to_string());
359 assert!(c.as_control().is_none());
360 }
361
362 #[test]
365 fn run_content_display_text_short() {
366 let c = RunContent::Text("hello".to_string());
367 assert_eq!(c.to_string(), "Text(\"hello\")");
368 }
369
370 #[test]
371 fn run_content_display_text_long_truncated() {
372 let long = "A".repeat(100);
373 let c = RunContent::Text(long);
374 let s = c.to_string();
375 assert!(s.contains(&"A".repeat(50)), "display: {s}");
376 assert!(s.ends_with("...\")"), "display: {s}");
377 }
378
379 #[test]
380 fn run_display() {
381 let run = Run::text("test", CharShapeIndex::new(0));
382 let s = run.to_string();
383 assert!(s.contains("Run("), "display: {s}");
384 assert!(s.contains("Text"), "display: {s}");
385 }
386
387 #[test]
390 fn empty_text_run() {
391 let run = Run::text("", CharShapeIndex::new(0));
392 assert_eq!(run.content.as_text(), Some(""));
393 }
394
395 #[test]
398 fn korean_text_run() {
399 let run = Run::text("안녕하세요", CharShapeIndex::new(0));
400 assert_eq!(run.content.as_text(), Some("안녕하세요"));
401 }
402
403 #[test]
406 fn run_equality() {
407 let a = Run::text("hello", CharShapeIndex::new(0));
408 let b = Run::text("hello", CharShapeIndex::new(0));
409 let c = Run::text("world", CharShapeIndex::new(0));
410 let d = Run::text("hello", CharShapeIndex::new(1));
411 assert_eq!(a, b);
412 assert_ne!(a, c);
413 assert_ne!(a, d);
414 }
415
416 #[test]
419 fn serde_roundtrip_text() {
420 let run = Run::text("test", CharShapeIndex::new(5));
421 let json = serde_json::to_string(&run).unwrap();
422 let back: Run = serde_json::from_str(&json).unwrap();
423 assert_eq!(run, back);
424 }
425
426 #[test]
427 fn serde_roundtrip_table() {
428 let run = Run::table(Table::new(vec![]), CharShapeIndex::new(0));
429 let json = serde_json::to_string(&run).unwrap();
430 let back: Run = serde_json::from_str(&json).unwrap();
431 assert_eq!(run, back);
432 }
433
434 #[test]
435 fn serde_roundtrip_image() {
436 let img = Image::new("test.png", HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Png);
437 let run = Run::image(img, CharShapeIndex::new(0));
438 let json = serde_json::to_string(&run).unwrap();
439 let back: Run = serde_json::from_str(&json).unwrap();
440 assert_eq!(run, back);
441 }
442
443 #[test]
444 fn serde_roundtrip_control() {
445 let ctrl =
446 Control::Hyperlink { text: "link".to_string(), url: "https://example.com".to_string() };
447 let run = Run::control(ctrl, CharShapeIndex::new(0));
448 let json = serde_json::to_string(&run).unwrap();
449 let back: Run = serde_json::from_str(&json).unwrap();
450 assert_eq!(run, back);
451 }
452
453 #[test]
456 fn run_clone_independence() {
457 let run = Run::text("original", CharShapeIndex::new(0));
458 let cloned = run.clone();
459 assert_eq!(run, cloned);
460 }
461}