use hwpforge_foundation::HwpUnit;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
pub struct InlineText {
pub segments: Vec<InlineSegment>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[non_exhaustive]
pub enum InlineSegment {
Plain(String),
Tab(InlineTabAttr),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct InlineTabAttr {
pub width: HwpUnit,
pub leader: u8,
pub tab_type: u8,
}
impl InlineTabAttr {
pub fn is_default(&self) -> bool {
self.width.as_i32() == 0 && self.leader == 0 && self.tab_type == 0
}
}
impl InlineText {
pub fn from_segments(segments: impl IntoIterator<Item = InlineSegment>) -> Self {
let mut out: Vec<InlineSegment> = Vec::new();
for seg in segments {
match seg {
InlineSegment::Plain(s) if s.is_empty() => continue,
InlineSegment::Plain(s) => match out.last_mut() {
Some(InlineSegment::Plain(prev)) => prev.push_str(&s),
_ => out.push(InlineSegment::Plain(s)),
},
other => out.push(other),
}
}
Self { segments: out }
}
pub fn plain_text(&self) -> String {
let mut out = String::new();
for seg in &self.segments {
match seg {
InlineSegment::Plain(s) => out.push_str(s),
InlineSegment::Tab(_) => out.push('\t'),
}
}
out
}
pub fn is_downgradable(&self) -> bool {
self.segments.iter().all(|seg| match seg {
InlineSegment::Plain(_) => true,
InlineSegment::Tab(attr) => attr.is_default(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn attr(width: i32, leader: u8, tab_type: u8) -> InlineTabAttr {
InlineTabAttr { width: HwpUnit::new(width).unwrap(), leader, tab_type }
}
#[test]
fn from_segments_merges_adjacent_plain_runs() {
let it = InlineText::from_segments([
InlineSegment::Plain("LE".into()),
InlineSegment::Plain("FT".into()),
InlineSegment::Tab(attr(12488, 3, 1)),
InlineSegment::Plain("RI".into()),
InlineSegment::Plain("GHT".into()),
]);
assert_eq!(
it.segments,
vec![
InlineSegment::Plain("LEFT".into()),
InlineSegment::Tab(attr(12488, 3, 1)),
InlineSegment::Plain("RIGHT".into()),
]
);
}
#[test]
fn from_segments_drops_empty_plain_entries() {
let it = InlineText::from_segments([
InlineSegment::Plain(String::new()),
InlineSegment::Tab(attr(0, 0, 0)),
InlineSegment::Plain(String::new()),
]);
assert_eq!(it.segments, vec![InlineSegment::Tab(attr(0, 0, 0))]);
}
#[test]
fn plain_text_renders_tabs_as_horizontal_tab_chars() {
let it = InlineText::from_segments([
InlineSegment::Plain("a".into()),
InlineSegment::Tab(attr(12488, 3, 1)),
InlineSegment::Plain("b".into()),
InlineSegment::Tab(attr(0, 0, 0)),
InlineSegment::Plain("c".into()),
]);
assert_eq!(it.plain_text(), "a\tb\tc");
}
#[test]
fn is_default_attr() {
assert!(attr(0, 0, 0).is_default());
assert!(!attr(1, 0, 0).is_default());
assert!(!attr(0, 1, 0).is_default());
assert!(!attr(0, 0, 1).is_default());
}
#[test]
fn is_downgradable_true_for_plain_and_default_tab_only() {
assert!(InlineText::from_segments([InlineSegment::Plain("hi".into())]).is_downgradable());
assert!(InlineText::from_segments([
InlineSegment::Plain("a".into()),
InlineSegment::Tab(attr(0, 0, 0)),
InlineSegment::Plain("b".into()),
])
.is_downgradable());
assert!(
!InlineText::from_segments([InlineSegment::Tab(attr(12488, 3, 1))]).is_downgradable()
);
}
}