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 fn text(s: impl Into<String>, char_shape_id: CharShapeIndex) -> Self {
89 Self { content: RunContent::Text(s.into()), char_shape_id }
90 }
91
92 pub fn table(table: Table, char_shape_id: CharShapeIndex) -> Self {
106 Self { content: RunContent::Table(Box::new(table)), char_shape_id }
107 }
108
109 pub fn image(image: Image, char_shape_id: CharShapeIndex) -> Self {
123 Self { content: RunContent::Image(image), char_shape_id }
124 }
125
126 pub fn control(control: Control, char_shape_id: CharShapeIndex) -> Self {
143 Self { content: RunContent::Control(Box::new(control)), char_shape_id }
144 }
145}
146
147impl std::fmt::Display for Run {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 write!(f, "Run({})", self.content)
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
174#[non_exhaustive]
175pub enum RunContent {
176 Text(String),
178 InlineText(InlineText),
186 Table(Box<Table>),
188 Image(Image),
190 Control(Box<Control>),
192}
193
194impl RunContent {
195 pub fn as_text(&self) -> Option<&str> {
209 match self {
210 Self::Text(s) => Some(s),
211 _ => None,
212 }
213 }
214
215 pub fn as_inline_text(&self) -> Option<&InlineText> {
217 match self {
218 Self::InlineText(it) => Some(it),
219 _ => None,
220 }
221 }
222
223 pub fn plain_text(&self) -> Option<std::borrow::Cow<'_, str>> {
231 match self {
232 Self::Text(s) => Some(std::borrow::Cow::Borrowed(s)),
233 Self::InlineText(it) => Some(std::borrow::Cow::Owned(it.plain_text())),
234 _ => None,
235 }
236 }
237
238 pub fn as_table(&self) -> Option<&Table> {
240 match self {
241 Self::Table(t) => Some(t),
242 _ => None,
243 }
244 }
245
246 pub fn as_image(&self) -> Option<&Image> {
248 match self {
249 Self::Image(i) => Some(i),
250 _ => None,
251 }
252 }
253
254 pub fn as_control(&self) -> Option<&Control> {
256 match self {
257 Self::Control(c) => Some(c),
258 _ => None,
259 }
260 }
261
262 pub fn is_text(&self) -> bool {
264 matches!(self, Self::Text(_))
265 }
266
267 pub fn is_inline_text(&self) -> bool {
269 matches!(self, Self::InlineText(_))
270 }
271
272 pub fn carries_text(&self) -> bool {
274 matches!(self, Self::Text(_) | Self::InlineText(_))
275 }
276
277 pub fn is_table(&self) -> bool {
279 matches!(self, Self::Table(_))
280 }
281
282 pub fn is_image(&self) -> bool {
284 matches!(self, Self::Image(_))
285 }
286
287 pub fn is_control(&self) -> bool {
289 matches!(self, Self::Control(_))
290 }
291}
292
293impl std::fmt::Display for RunContent {
294 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295 match self {
296 Self::Text(s) => {
297 if s.len() <= 50 {
298 write!(f, "Text(\"{s}\")")
299 } else {
300 let truncated: String = s.chars().take(50).collect();
301 write!(f, "Text(\"{truncated}...\")")
302 }
303 }
304 Self::InlineText(it) => {
305 let plain = it.plain_text();
306 let tabs = it
307 .segments
308 .iter()
309 .filter(|s| matches!(s, crate::inline::InlineSegment::Tab(_)))
310 .count();
311 if plain.len() <= 50 {
312 write!(f, "InlineText(\"{plain}\", tabs={tabs})")
313 } else {
314 let truncated: String = plain.chars().take(50).collect();
315 write!(f, "InlineText(\"{truncated}...\", tabs={tabs})")
316 }
317 }
318 Self::Table(t) => write!(f, "{t}"),
319 Self::Image(i) => write!(f, "{i}"),
320 Self::Control(c) => write!(f, "{c}"),
321 }
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use crate::image::ImageFormat;
329 use hwpforge_foundation::HwpUnit;
330
331 #[test]
332 fn run_text_constructor() {
333 let run = Run::text("Hello", CharShapeIndex::new(0));
334 assert_eq!(run.content.as_text(), Some("Hello"));
335 assert_eq!(run.char_shape_id, CharShapeIndex::new(0));
336 }
337
338 #[test]
339 fn run_text_from_string() {
340 let s = String::from("owned");
341 let run = Run::text(s, CharShapeIndex::new(1));
342 assert_eq!(run.content.as_text(), Some("owned"));
343 }
344
345 #[test]
346 fn run_table_constructor() {
347 let table = Table::new(vec![]);
348 let run = Run::table(table, CharShapeIndex::new(0));
349 assert!(run.content.is_table());
350 assert!(run.content.as_table().unwrap().is_empty());
351 }
352
353 #[test]
354 fn run_image_constructor() {
355 let img = Image::new("test.png", HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Png);
356 let run = Run::image(img, CharShapeIndex::new(0));
357 assert!(run.content.is_image());
358 assert_eq!(run.content.as_image().unwrap().path, "test.png");
359 }
360
361 #[test]
362 fn run_control_constructor() {
363 let ctrl =
364 Control::Hyperlink { text: "link".to_string(), url: "https://example.com".to_string() };
365 let run = Run::control(ctrl, CharShapeIndex::new(0));
366 assert!(run.content.is_control());
367 assert!(run.content.as_control().unwrap().is_hyperlink());
368 }
369
370 #[test]
373 fn run_content_text_checks() {
374 let c = RunContent::Text("hi".to_string());
375 assert!(c.is_text());
376 assert!(!c.is_table());
377 assert!(!c.is_image());
378 assert!(!c.is_control());
379 }
380
381 #[test]
382 fn run_content_table_checks() {
383 let c = RunContent::Table(Box::new(Table::new(vec![])));
384 assert!(!c.is_text());
385 assert!(c.is_table());
386 }
387
388 #[test]
389 fn run_content_image_checks() {
390 let c =
391 RunContent::Image(Image::new("x.png", HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Png));
392 assert!(!c.is_text());
393 assert!(c.is_image());
394 }
395
396 #[test]
397 fn run_content_control_checks() {
398 let c =
399 RunContent::Control(Box::new(Control::Unknown { tag: "x".to_string(), data: None }));
400 assert!(!c.is_text());
401 assert!(c.is_control());
402 }
403
404 #[test]
407 fn as_text_returns_none_for_non_text() {
408 let c = RunContent::Table(Box::new(Table::new(vec![])));
409 assert!(c.as_text().is_none());
410 }
411
412 #[test]
413 fn as_table_returns_none_for_non_table() {
414 let c = RunContent::Text("hi".to_string());
415 assert!(c.as_table().is_none());
416 }
417
418 #[test]
419 fn as_image_returns_none_for_non_image() {
420 let c = RunContent::Text("hi".to_string());
421 assert!(c.as_image().is_none());
422 }
423
424 #[test]
425 fn as_control_returns_none_for_non_control() {
426 let c = RunContent::Text("hi".to_string());
427 assert!(c.as_control().is_none());
428 }
429
430 #[test]
433 fn run_content_display_text_short() {
434 let c = RunContent::Text("hello".to_string());
435 assert_eq!(c.to_string(), "Text(\"hello\")");
436 }
437
438 #[test]
439 fn run_content_display_text_long_truncated() {
440 let long = "A".repeat(100);
441 let c = RunContent::Text(long);
442 let s = c.to_string();
443 assert!(s.contains(&"A".repeat(50)), "display: {s}");
444 assert!(s.ends_with("...\")"), "display: {s}");
445 }
446
447 #[test]
448 fn run_display() {
449 let run = Run::text("test", CharShapeIndex::new(0));
450 let s = run.to_string();
451 assert!(s.contains("Run("), "display: {s}");
452 assert!(s.contains("Text"), "display: {s}");
453 }
454
455 #[test]
458 fn empty_text_run() {
459 let run = Run::text("", CharShapeIndex::new(0));
460 assert_eq!(run.content.as_text(), Some(""));
461 }
462
463 #[test]
466 fn korean_text_run() {
467 let run = Run::text("안녕하세요", CharShapeIndex::new(0));
468 assert_eq!(run.content.as_text(), Some("안녕하세요"));
469 }
470
471 #[test]
474 fn run_equality() {
475 let a = Run::text("hello", CharShapeIndex::new(0));
476 let b = Run::text("hello", CharShapeIndex::new(0));
477 let c = Run::text("world", CharShapeIndex::new(0));
478 let d = Run::text("hello", CharShapeIndex::new(1));
479 assert_eq!(a, b);
480 assert_ne!(a, c);
481 assert_ne!(a, d);
482 }
483
484 #[test]
487 fn serde_roundtrip_text() {
488 let run = Run::text("test", CharShapeIndex::new(5));
489 let json = serde_json::to_string(&run).unwrap();
490 let back: Run = serde_json::from_str(&json).unwrap();
491 assert_eq!(run, back);
492 }
493
494 #[test]
495 fn serde_roundtrip_table() {
496 let run = Run::table(Table::new(vec![]), CharShapeIndex::new(0));
497 let json = serde_json::to_string(&run).unwrap();
498 let back: Run = serde_json::from_str(&json).unwrap();
499 assert_eq!(run, back);
500 }
501
502 #[test]
503 fn serde_roundtrip_image() {
504 let img = Image::new("test.png", HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Png);
505 let run = Run::image(img, CharShapeIndex::new(0));
506 let json = serde_json::to_string(&run).unwrap();
507 let back: Run = serde_json::from_str(&json).unwrap();
508 assert_eq!(run, back);
509 }
510
511 #[test]
512 fn serde_roundtrip_control() {
513 let ctrl =
514 Control::Hyperlink { text: "link".to_string(), url: "https://example.com".to_string() };
515 let run = Run::control(ctrl, CharShapeIndex::new(0));
516 let json = serde_json::to_string(&run).unwrap();
517 let back: Run = serde_json::from_str(&json).unwrap();
518 assert_eq!(run, back);
519 }
520
521 #[test]
524 fn run_clone_independence() {
525 let run = Run::text("original", CharShapeIndex::new(0));
526 let cloned = run.clone();
527 assert_eq!(run, cloned);
528 }
529}