use hwpforge_foundation::CharShapeIndex;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::control::Control;
use crate::image::Image;
use crate::table::Table;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Run {
pub content: RunContent,
pub char_shape_id: CharShapeIndex,
}
impl Run {
pub fn text(s: impl Into<String>, char_shape_id: CharShapeIndex) -> Self {
Self { content: RunContent::Text(s.into()), char_shape_id }
}
pub fn table(table: Table, char_shape_id: CharShapeIndex) -> Self {
Self { content: RunContent::Table(Box::new(table)), char_shape_id }
}
pub fn image(image: Image, char_shape_id: CharShapeIndex) -> Self {
Self { content: RunContent::Image(image), char_shape_id }
}
pub fn control(control: Control, char_shape_id: CharShapeIndex) -> Self {
Self { content: RunContent::Control(Box::new(control)), char_shape_id }
}
}
impl std::fmt::Display for Run {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Run({})", self.content)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub enum RunContent {
Text(String),
Table(Box<Table>),
Image(Image),
Control(Box<Control>),
}
impl RunContent {
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(s) => Some(s),
_ => None,
}
}
pub fn as_table(&self) -> Option<&Table> {
match self {
Self::Table(t) => Some(t),
_ => None,
}
}
pub fn as_image(&self) -> Option<&Image> {
match self {
Self::Image(i) => Some(i),
_ => None,
}
}
pub fn as_control(&self) -> Option<&Control> {
match self {
Self::Control(c) => Some(c),
_ => None,
}
}
pub fn is_text(&self) -> bool {
matches!(self, Self::Text(_))
}
pub fn is_table(&self) -> bool {
matches!(self, Self::Table(_))
}
pub fn is_image(&self) -> bool {
matches!(self, Self::Image(_))
}
pub fn is_control(&self) -> bool {
matches!(self, Self::Control(_))
}
}
impl std::fmt::Display for RunContent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Text(s) => {
if s.len() <= 50 {
write!(f, "Text(\"{s}\")")
} else {
let truncated: String = s.chars().take(50).collect();
write!(f, "Text(\"{truncated}...\")")
}
}
Self::Table(t) => write!(f, "{t}"),
Self::Image(i) => write!(f, "{i}"),
Self::Control(c) => write!(f, "{c}"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::image::ImageFormat;
use hwpforge_foundation::HwpUnit;
#[test]
fn run_text_constructor() {
let run = Run::text("Hello", CharShapeIndex::new(0));
assert_eq!(run.content.as_text(), Some("Hello"));
assert_eq!(run.char_shape_id, CharShapeIndex::new(0));
}
#[test]
fn run_text_from_string() {
let s = String::from("owned");
let run = Run::text(s, CharShapeIndex::new(1));
assert_eq!(run.content.as_text(), Some("owned"));
}
#[test]
fn run_table_constructor() {
let table = Table::new(vec![]);
let run = Run::table(table, CharShapeIndex::new(0));
assert!(run.content.is_table());
assert!(run.content.as_table().unwrap().is_empty());
}
#[test]
fn run_image_constructor() {
let img = Image::new("test.png", HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Png);
let run = Run::image(img, CharShapeIndex::new(0));
assert!(run.content.is_image());
assert_eq!(run.content.as_image().unwrap().path, "test.png");
}
#[test]
fn run_control_constructor() {
let ctrl =
Control::Hyperlink { text: "link".to_string(), url: "https://example.com".to_string() };
let run = Run::control(ctrl, CharShapeIndex::new(0));
assert!(run.content.is_control());
assert!(run.content.as_control().unwrap().is_hyperlink());
}
#[test]
fn run_content_text_checks() {
let c = RunContent::Text("hi".to_string());
assert!(c.is_text());
assert!(!c.is_table());
assert!(!c.is_image());
assert!(!c.is_control());
}
#[test]
fn run_content_table_checks() {
let c = RunContent::Table(Box::new(Table::new(vec![])));
assert!(!c.is_text());
assert!(c.is_table());
}
#[test]
fn run_content_image_checks() {
let c =
RunContent::Image(Image::new("x.png", HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Png));
assert!(!c.is_text());
assert!(c.is_image());
}
#[test]
fn run_content_control_checks() {
let c =
RunContent::Control(Box::new(Control::Unknown { tag: "x".to_string(), data: None }));
assert!(!c.is_text());
assert!(c.is_control());
}
#[test]
fn as_text_returns_none_for_non_text() {
let c = RunContent::Table(Box::new(Table::new(vec![])));
assert!(c.as_text().is_none());
}
#[test]
fn as_table_returns_none_for_non_table() {
let c = RunContent::Text("hi".to_string());
assert!(c.as_table().is_none());
}
#[test]
fn as_image_returns_none_for_non_image() {
let c = RunContent::Text("hi".to_string());
assert!(c.as_image().is_none());
}
#[test]
fn as_control_returns_none_for_non_control() {
let c = RunContent::Text("hi".to_string());
assert!(c.as_control().is_none());
}
#[test]
fn run_content_display_text_short() {
let c = RunContent::Text("hello".to_string());
assert_eq!(c.to_string(), "Text(\"hello\")");
}
#[test]
fn run_content_display_text_long_truncated() {
let long = "A".repeat(100);
let c = RunContent::Text(long);
let s = c.to_string();
assert!(s.contains(&"A".repeat(50)), "display: {s}");
assert!(s.ends_with("...\")"), "display: {s}");
}
#[test]
fn run_display() {
let run = Run::text("test", CharShapeIndex::new(0));
let s = run.to_string();
assert!(s.contains("Run("), "display: {s}");
assert!(s.contains("Text"), "display: {s}");
}
#[test]
fn empty_text_run() {
let run = Run::text("", CharShapeIndex::new(0));
assert_eq!(run.content.as_text(), Some(""));
}
#[test]
fn korean_text_run() {
let run = Run::text("안녕하세요", CharShapeIndex::new(0));
assert_eq!(run.content.as_text(), Some("안녕하세요"));
}
#[test]
fn run_equality() {
let a = Run::text("hello", CharShapeIndex::new(0));
let b = Run::text("hello", CharShapeIndex::new(0));
let c = Run::text("world", CharShapeIndex::new(0));
let d = Run::text("hello", CharShapeIndex::new(1));
assert_eq!(a, b);
assert_ne!(a, c);
assert_ne!(a, d);
}
#[test]
fn serde_roundtrip_text() {
let run = Run::text("test", CharShapeIndex::new(5));
let json = serde_json::to_string(&run).unwrap();
let back: Run = serde_json::from_str(&json).unwrap();
assert_eq!(run, back);
}
#[test]
fn serde_roundtrip_table() {
let run = Run::table(Table::new(vec![]), CharShapeIndex::new(0));
let json = serde_json::to_string(&run).unwrap();
let back: Run = serde_json::from_str(&json).unwrap();
assert_eq!(run, back);
}
#[test]
fn serde_roundtrip_image() {
let img = Image::new("test.png", HwpUnit::ZERO, HwpUnit::ZERO, ImageFormat::Png);
let run = Run::image(img, CharShapeIndex::new(0));
let json = serde_json::to_string(&run).unwrap();
let back: Run = serde_json::from_str(&json).unwrap();
assert_eq!(run, back);
}
#[test]
fn serde_roundtrip_control() {
let ctrl =
Control::Hyperlink { text: "link".to_string(), url: "https://example.com".to_string() };
let run = Run::control(ctrl, CharShapeIndex::new(0));
let json = serde_json::to_string(&run).unwrap();
let back: Run = serde_json::from_str(&json).unwrap();
assert_eq!(run, back);
}
#[test]
fn run_clone_independence() {
let run = Run::text("original", CharShapeIndex::new(0));
let cloned = run.clone();
assert_eq!(run, cloned);
}
}