1use hwpforge_foundation::CharShapeIndex;
30use schemars::JsonSchema;
31use serde::{Deserialize, Serialize};
32
33use crate::control::Control;
34use crate::image::Image;
35use crate::inline::InlineText;
36use crate::table::Table;
37
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
54pub struct Run {
55 pub content: RunContent,
57 pub char_shape_id: CharShapeIndex,
59}
60
61impl Run {
62 pub(crate) fn walk_paragraphs_mut(
64 &mut self,
65 f: &mut dyn FnMut(&mut crate::paragraph::Paragraph),
66 ) {
67 match &mut self.content {
68 RunContent::Table(table) => table.walk_paragraphs_mut(f),
69 RunContent::Control(control) => control.walk_paragraphs_mut(f),
70 RunContent::Text(_) | RunContent::InlineText(_) | RunContent::Image(_) => {}
71 }
72 }
73
74 pub(crate) fn walk_paragraphs(&self, f: &mut dyn FnMut(&crate::paragraph::Paragraph)) {
80 match &self.content {
81 RunContent::Table(table) => table.walk_paragraphs(f),
82 RunContent::Control(control) => control.walk_paragraphs(f),
83 RunContent::Text(_) | RunContent::InlineText(_) | RunContent::Image(_) => {}
84 }
85 }
86
87 pub fn text(s: impl Into<String>, char_shape_id: CharShapeIndex) -> Self {
102 Self { content: RunContent::Text(s.into()), char_shape_id }
103 }
104
105 pub fn table(table: Table, char_shape_id: CharShapeIndex) -> Self {
119 Self { content: RunContent::Table(Box::new(table)), char_shape_id }
120 }
121
122 pub fn image(image: Image, char_shape_id: CharShapeIndex) -> Self {
136 Self { content: RunContent::Image(image), char_shape_id }
137 }
138
139 pub fn control(control: Control, char_shape_id: CharShapeIndex) -> Self {
156 Self { content: RunContent::Control(Box::new(control)), char_shape_id }
157 }
158}
159
160impl std::fmt::Display for Run {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 write!(f, "Run({})", self.content)
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
187#[non_exhaustive]
188pub enum RunContent {
189 Text(String),
191 InlineText(InlineText),
199 Table(Box<Table>),
201 Image(Image),
203 Control(Box<Control>),
205}
206
207impl RunContent {
208 pub fn as_text(&self) -> Option<&str> {
222 match self {
223 Self::Text(s) => Some(s),
224 _ => None,
225 }
226 }
227
228 pub fn as_inline_text(&self) -> Option<&InlineText> {
230 match self {
231 Self::InlineText(it) => Some(it),
232 _ => None,
233 }
234 }
235
236 pub fn plain_text(&self) -> Option<std::borrow::Cow<'_, str>> {
244 match self {
245 Self::Text(s) => Some(std::borrow::Cow::Borrowed(s)),
246 Self::InlineText(it) => Some(std::borrow::Cow::Owned(it.plain_text())),
247 _ => None,
248 }
249 }
250
251 pub fn as_table(&self) -> Option<&Table> {
253 match self {
254 Self::Table(t) => Some(t),
255 _ => None,
256 }
257 }
258
259 pub fn as_image(&self) -> Option<&Image> {
261 match self {
262 Self::Image(i) => Some(i),
263 _ => None,
264 }
265 }
266
267 pub fn as_control(&self) -> Option<&Control> {
269 match self {
270 Self::Control(c) => Some(c),
271 _ => None,
272 }
273 }
274
275 pub fn is_text(&self) -> bool {
277 matches!(self, Self::Text(_))
278 }
279
280 pub fn is_inline_text(&self) -> bool {
282 matches!(self, Self::InlineText(_))
283 }
284
285 pub fn carries_text(&self) -> bool {
287 matches!(self, Self::Text(_) | Self::InlineText(_))
288 }
289
290 pub fn is_table(&self) -> bool {
292 matches!(self, Self::Table(_))
293 }
294
295 pub fn is_image(&self) -> bool {
297 matches!(self, Self::Image(_))
298 }
299
300 pub fn is_control(&self) -> bool {
302 matches!(self, Self::Control(_))
303 }
304}
305
306impl std::fmt::Display for RunContent {
307 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308 match self {
309 Self::Text(s) => {
310 if s.len() <= 50 {
311 write!(f, "Text(\"{s}\")")
312 } else {
313 let truncated: String = s.chars().take(50).collect();
314 write!(f, "Text(\"{truncated}...\")")
315 }
316 }
317 Self::InlineText(it) => {
318 let plain = it.plain_text();
319 let tabs = it
320 .segments
321 .iter()
322 .filter(|s| matches!(s, crate::inline::InlineSegment::Tab(_)))
323 .count();
324 if plain.len() <= 50 {
325 write!(f, "InlineText(\"{plain}\", tabs={tabs})")
326 } else {
327 let truncated: String = plain.chars().take(50).collect();
328 write!(f, "InlineText(\"{truncated}...\", tabs={tabs})")
329 }
330 }
331 Self::Table(t) => write!(f, "{t}"),
332 Self::Image(i) => write!(f, "{i}"),
333 Self::Control(c) => write!(f, "{c}"),
334 }
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use crate::image::ImageFormat;
342 use hwpforge_foundation::HwpUnit;
343
344 #[test]
345 fn run_text_constructor() {
346 let run = Run::text("Hello", CharShapeIndex::new(0));
347 assert_eq!(run.content.as_text(), Some("Hello"));
348 assert_eq!(run.char_shape_id, CharShapeIndex::new(0));
349 }
350
351 #[test]
352 fn run_text_from_string() {
353 let s = String::from("owned");
354 let run = Run::text(s, CharShapeIndex::new(1));
355 assert_eq!(run.content.as_text(), Some("owned"));
356 }
357
358 #[test]
359 fn run_table_constructor() {
360 let table = Table::new(vec![]);
361 let run = Run::table(table, CharShapeIndex::new(0));
362 assert!(run.content.is_table());
363 assert!(run.content.as_table().unwrap().is_empty());
364 }
365
366 #[test]
367 fn run_image_constructor() {
368 let img = Image::new("test.png", HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Png);
369 let run = Run::image(img, CharShapeIndex::new(0));
370 assert!(run.content.is_image());
371 assert_eq!(run.content.as_image().unwrap().path, "test.png");
372 }
373
374 #[test]
375 fn run_control_constructor() {
376 let ctrl =
377 Control::Hyperlink { text: "link".to_string(), url: "https://example.com".to_string() };
378 let run = Run::control(ctrl, CharShapeIndex::new(0));
379 assert!(run.content.is_control());
380 assert!(run.content.as_control().unwrap().is_hyperlink());
381 }
382
383 #[test]
386 fn run_content_text_checks() {
387 let c = RunContent::Text("hi".to_string());
388 assert!(c.is_text());
389 assert!(!c.is_table());
390 assert!(!c.is_image());
391 assert!(!c.is_control());
392 }
393
394 #[test]
395 fn run_content_table_checks() {
396 let c = RunContent::Table(Box::new(Table::new(vec![])));
397 assert!(!c.is_text());
398 assert!(c.is_table());
399 }
400
401 #[test]
402 fn run_content_image_checks() {
403 let c =
404 RunContent::Image(Image::new("x.png", HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Png));
405 assert!(!c.is_text());
406 assert!(c.is_image());
407 }
408
409 #[test]
410 fn run_content_control_checks() {
411 let c =
412 RunContent::Control(Box::new(Control::Unknown { tag: "x".to_string(), data: None }));
413 assert!(!c.is_text());
414 assert!(c.is_control());
415 }
416
417 #[test]
420 fn as_text_returns_none_for_non_text() {
421 let c = RunContent::Table(Box::new(Table::new(vec![])));
422 assert!(c.as_text().is_none());
423 }
424
425 #[test]
426 fn as_table_returns_none_for_non_table() {
427 let c = RunContent::Text("hi".to_string());
428 assert!(c.as_table().is_none());
429 }
430
431 #[test]
432 fn as_image_returns_none_for_non_image() {
433 let c = RunContent::Text("hi".to_string());
434 assert!(c.as_image().is_none());
435 }
436
437 #[test]
438 fn as_control_returns_none_for_non_control() {
439 let c = RunContent::Text("hi".to_string());
440 assert!(c.as_control().is_none());
441 }
442
443 #[test]
446 fn run_content_display_text_short() {
447 let c = RunContent::Text("hello".to_string());
448 assert_eq!(c.to_string(), "Text(\"hello\")");
449 }
450
451 #[test]
452 fn run_content_display_text_long_truncated() {
453 let long = "A".repeat(100);
454 let c = RunContent::Text(long);
455 let s = c.to_string();
456 assert!(s.contains(&"A".repeat(50)), "display: {s}");
457 assert!(s.ends_with("...\")"), "display: {s}");
458 }
459
460 #[test]
461 fn run_display() {
462 let run = Run::text("test", CharShapeIndex::new(0));
463 let s = run.to_string();
464 assert!(s.contains("Run("), "display: {s}");
465 assert!(s.contains("Text"), "display: {s}");
466 }
467
468 #[test]
471 fn empty_text_run() {
472 let run = Run::text("", CharShapeIndex::new(0));
473 assert_eq!(run.content.as_text(), Some(""));
474 }
475
476 #[test]
479 fn korean_text_run() {
480 let run = Run::text("안녕하세요", CharShapeIndex::new(0));
481 assert_eq!(run.content.as_text(), Some("안녕하세요"));
482 }
483
484 #[test]
487 fn run_equality() {
488 let a = Run::text("hello", CharShapeIndex::new(0));
489 let b = Run::text("hello", CharShapeIndex::new(0));
490 let c = Run::text("world", CharShapeIndex::new(0));
491 let d = Run::text("hello", CharShapeIndex::new(1));
492 assert_eq!(a, b);
493 assert_ne!(a, c);
494 assert_ne!(a, d);
495 }
496
497 #[test]
500 fn serde_roundtrip_text() {
501 let run = Run::text("test", CharShapeIndex::new(5));
502 let json = serde_json::to_string(&run).unwrap();
503 let back: Run = serde_json::from_str(&json).unwrap();
504 assert_eq!(run, back);
505 }
506
507 #[test]
508 fn serde_roundtrip_table() {
509 let run = Run::table(Table::new(vec![]), CharShapeIndex::new(0));
510 let json = serde_json::to_string(&run).unwrap();
511 let back: Run = serde_json::from_str(&json).unwrap();
512 assert_eq!(run, back);
513 }
514
515 #[test]
516 fn serde_roundtrip_image() {
517 let img = Image::new("test.png", HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Png);
518 let run = Run::image(img, CharShapeIndex::new(0));
519 let json = serde_json::to_string(&run).unwrap();
520 let back: Run = serde_json::from_str(&json).unwrap();
521 assert_eq!(run, back);
522 }
523
524 #[test]
525 fn serde_roundtrip_control() {
526 let ctrl =
527 Control::Hyperlink { text: "link".to_string(), url: "https://example.com".to_string() };
528 let run = Run::control(ctrl, CharShapeIndex::new(0));
529 let json = serde_json::to_string(&run).unwrap();
530 let back: Run = serde_json::from_str(&json).unwrap();
531 assert_eq!(run, back);
532 }
533
534 #[test]
537 fn run_clone_independence() {
538 let run = Run::text("original", CharShapeIndex::new(0));
539 let cloned = run.clone();
540 assert_eq!(run, cloned);
541 }
542}