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 fn text(s: impl Into<String>, char_shape_id: CharShapeIndex) -> Self {
77 Self { content: RunContent::Text(s.into()), char_shape_id }
78 }
79
80 pub fn table(table: Table, char_shape_id: CharShapeIndex) -> Self {
94 Self { content: RunContent::Table(Box::new(table)), char_shape_id }
95 }
96
97 pub fn image(image: Image, char_shape_id: CharShapeIndex) -> Self {
111 Self { content: RunContent::Image(image), char_shape_id }
112 }
113
114 pub fn control(control: Control, char_shape_id: CharShapeIndex) -> Self {
131 Self { content: RunContent::Control(Box::new(control)), char_shape_id }
132 }
133}
134
135impl std::fmt::Display for Run {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 write!(f, "Run({})", self.content)
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
162#[non_exhaustive]
163pub enum RunContent {
164 Text(String),
166 InlineText(InlineText),
174 Table(Box<Table>),
176 Image(Image),
178 Control(Box<Control>),
180}
181
182impl RunContent {
183 pub fn as_text(&self) -> Option<&str> {
197 match self {
198 Self::Text(s) => Some(s),
199 _ => None,
200 }
201 }
202
203 pub fn as_inline_text(&self) -> Option<&InlineText> {
205 match self {
206 Self::InlineText(it) => Some(it),
207 _ => None,
208 }
209 }
210
211 pub fn plain_text(&self) -> Option<std::borrow::Cow<'_, str>> {
219 match self {
220 Self::Text(s) => Some(std::borrow::Cow::Borrowed(s)),
221 Self::InlineText(it) => Some(std::borrow::Cow::Owned(it.plain_text())),
222 _ => None,
223 }
224 }
225
226 pub fn as_table(&self) -> Option<&Table> {
228 match self {
229 Self::Table(t) => Some(t),
230 _ => None,
231 }
232 }
233
234 pub fn as_image(&self) -> Option<&Image> {
236 match self {
237 Self::Image(i) => Some(i),
238 _ => None,
239 }
240 }
241
242 pub fn as_control(&self) -> Option<&Control> {
244 match self {
245 Self::Control(c) => Some(c),
246 _ => None,
247 }
248 }
249
250 pub fn is_text(&self) -> bool {
252 matches!(self, Self::Text(_))
253 }
254
255 pub fn is_inline_text(&self) -> bool {
257 matches!(self, Self::InlineText(_))
258 }
259
260 pub fn carries_text(&self) -> bool {
262 matches!(self, Self::Text(_) | Self::InlineText(_))
263 }
264
265 pub fn is_table(&self) -> bool {
267 matches!(self, Self::Table(_))
268 }
269
270 pub fn is_image(&self) -> bool {
272 matches!(self, Self::Image(_))
273 }
274
275 pub fn is_control(&self) -> bool {
277 matches!(self, Self::Control(_))
278 }
279}
280
281impl std::fmt::Display for RunContent {
282 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
283 match self {
284 Self::Text(s) => {
285 if s.len() <= 50 {
286 write!(f, "Text(\"{s}\")")
287 } else {
288 let truncated: String = s.chars().take(50).collect();
289 write!(f, "Text(\"{truncated}...\")")
290 }
291 }
292 Self::InlineText(it) => {
293 let plain = it.plain_text();
294 let tabs = it
295 .segments
296 .iter()
297 .filter(|s| matches!(s, crate::inline::InlineSegment::Tab(_)))
298 .count();
299 if plain.len() <= 50 {
300 write!(f, "InlineText(\"{plain}\", tabs={tabs})")
301 } else {
302 let truncated: String = plain.chars().take(50).collect();
303 write!(f, "InlineText(\"{truncated}...\", tabs={tabs})")
304 }
305 }
306 Self::Table(t) => write!(f, "{t}"),
307 Self::Image(i) => write!(f, "{i}"),
308 Self::Control(c) => write!(f, "{c}"),
309 }
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use crate::image::ImageFormat;
317 use hwpforge_foundation::HwpUnit;
318
319 #[test]
320 fn run_text_constructor() {
321 let run = Run::text("Hello", CharShapeIndex::new(0));
322 assert_eq!(run.content.as_text(), Some("Hello"));
323 assert_eq!(run.char_shape_id, CharShapeIndex::new(0));
324 }
325
326 #[test]
327 fn run_text_from_string() {
328 let s = String::from("owned");
329 let run = Run::text(s, CharShapeIndex::new(1));
330 assert_eq!(run.content.as_text(), Some("owned"));
331 }
332
333 #[test]
334 fn run_table_constructor() {
335 let table = Table::new(vec![]);
336 let run = Run::table(table, CharShapeIndex::new(0));
337 assert!(run.content.is_table());
338 assert!(run.content.as_table().unwrap().is_empty());
339 }
340
341 #[test]
342 fn run_image_constructor() {
343 let img = Image::new("test.png", HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Png);
344 let run = Run::image(img, CharShapeIndex::new(0));
345 assert!(run.content.is_image());
346 assert_eq!(run.content.as_image().unwrap().path, "test.png");
347 }
348
349 #[test]
350 fn run_control_constructor() {
351 let ctrl =
352 Control::Hyperlink { text: "link".to_string(), url: "https://example.com".to_string() };
353 let run = Run::control(ctrl, CharShapeIndex::new(0));
354 assert!(run.content.is_control());
355 assert!(run.content.as_control().unwrap().is_hyperlink());
356 }
357
358 #[test]
361 fn run_content_text_checks() {
362 let c = RunContent::Text("hi".to_string());
363 assert!(c.is_text());
364 assert!(!c.is_table());
365 assert!(!c.is_image());
366 assert!(!c.is_control());
367 }
368
369 #[test]
370 fn run_content_table_checks() {
371 let c = RunContent::Table(Box::new(Table::new(vec![])));
372 assert!(!c.is_text());
373 assert!(c.is_table());
374 }
375
376 #[test]
377 fn run_content_image_checks() {
378 let c =
379 RunContent::Image(Image::new("x.png", HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Png));
380 assert!(!c.is_text());
381 assert!(c.is_image());
382 }
383
384 #[test]
385 fn run_content_control_checks() {
386 let c =
387 RunContent::Control(Box::new(Control::Unknown { tag: "x".to_string(), data: None }));
388 assert!(!c.is_text());
389 assert!(c.is_control());
390 }
391
392 #[test]
395 fn as_text_returns_none_for_non_text() {
396 let c = RunContent::Table(Box::new(Table::new(vec![])));
397 assert!(c.as_text().is_none());
398 }
399
400 #[test]
401 fn as_table_returns_none_for_non_table() {
402 let c = RunContent::Text("hi".to_string());
403 assert!(c.as_table().is_none());
404 }
405
406 #[test]
407 fn as_image_returns_none_for_non_image() {
408 let c = RunContent::Text("hi".to_string());
409 assert!(c.as_image().is_none());
410 }
411
412 #[test]
413 fn as_control_returns_none_for_non_control() {
414 let c = RunContent::Text("hi".to_string());
415 assert!(c.as_control().is_none());
416 }
417
418 #[test]
421 fn run_content_display_text_short() {
422 let c = RunContent::Text("hello".to_string());
423 assert_eq!(c.to_string(), "Text(\"hello\")");
424 }
425
426 #[test]
427 fn run_content_display_text_long_truncated() {
428 let long = "A".repeat(100);
429 let c = RunContent::Text(long);
430 let s = c.to_string();
431 assert!(s.contains(&"A".repeat(50)), "display: {s}");
432 assert!(s.ends_with("...\")"), "display: {s}");
433 }
434
435 #[test]
436 fn run_display() {
437 let run = Run::text("test", CharShapeIndex::new(0));
438 let s = run.to_string();
439 assert!(s.contains("Run("), "display: {s}");
440 assert!(s.contains("Text"), "display: {s}");
441 }
442
443 #[test]
446 fn empty_text_run() {
447 let run = Run::text("", CharShapeIndex::new(0));
448 assert_eq!(run.content.as_text(), Some(""));
449 }
450
451 #[test]
454 fn korean_text_run() {
455 let run = Run::text("안녕하세요", CharShapeIndex::new(0));
456 assert_eq!(run.content.as_text(), Some("안녕하세요"));
457 }
458
459 #[test]
462 fn run_equality() {
463 let a = Run::text("hello", CharShapeIndex::new(0));
464 let b = Run::text("hello", CharShapeIndex::new(0));
465 let c = Run::text("world", CharShapeIndex::new(0));
466 let d = Run::text("hello", CharShapeIndex::new(1));
467 assert_eq!(a, b);
468 assert_ne!(a, c);
469 assert_ne!(a, d);
470 }
471
472 #[test]
475 fn serde_roundtrip_text() {
476 let run = Run::text("test", CharShapeIndex::new(5));
477 let json = serde_json::to_string(&run).unwrap();
478 let back: Run = serde_json::from_str(&json).unwrap();
479 assert_eq!(run, back);
480 }
481
482 #[test]
483 fn serde_roundtrip_table() {
484 let run = Run::table(Table::new(vec![]), CharShapeIndex::new(0));
485 let json = serde_json::to_string(&run).unwrap();
486 let back: Run = serde_json::from_str(&json).unwrap();
487 assert_eq!(run, back);
488 }
489
490 #[test]
491 fn serde_roundtrip_image() {
492 let img = Image::new("test.png", HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Png);
493 let run = Run::image(img, CharShapeIndex::new(0));
494 let json = serde_json::to_string(&run).unwrap();
495 let back: Run = serde_json::from_str(&json).unwrap();
496 assert_eq!(run, back);
497 }
498
499 #[test]
500 fn serde_roundtrip_control() {
501 let ctrl =
502 Control::Hyperlink { text: "link".to_string(), url: "https://example.com".to_string() };
503 let run = Run::control(ctrl, CharShapeIndex::new(0));
504 let json = serde_json::to_string(&run).unwrap();
505 let back: Run = serde_json::from_str(&json).unwrap();
506 assert_eq!(run, back);
507 }
508
509 #[test]
512 fn run_clone_independence() {
513 let run = Run::text("original", CharShapeIndex::new(0));
514 let cloned = run.clone();
515 assert_eq!(run, cloned);
516 }
517}