#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{format, vec::Vec};
use super::Span;
#[cfg(debug_assertions)]
use core::ops::Range;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Event<'a> {
pub event_type: EventType,
pub layer: &'a str,
pub start: &'a str,
pub end: &'a str,
pub style: &'a str,
pub name: &'a str,
pub margin_l: &'a str,
pub margin_r: &'a str,
pub margin_v: &'a str,
pub margin_t: Option<&'a str>,
pub margin_b: Option<&'a str>,
pub effect: &'a str,
pub text: &'a str,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EventType {
Dialogue,
Comment,
Picture,
Sound,
Movie,
Command,
}
impl EventType {
#[must_use]
pub fn parse_type(s: &str) -> Option<Self> {
match s.trim() {
"Dialogue" => Some(Self::Dialogue),
"Comment" => Some(Self::Comment),
"Picture" => Some(Self::Picture),
"Sound" => Some(Self::Sound),
"Movie" => Some(Self::Movie),
"Command" => Some(Self::Command),
_ => None,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Dialogue => "Dialogue",
Self::Comment => "Comment",
Self::Picture => "Picture",
Self::Sound => "Sound",
Self::Movie => "Movie",
Self::Command => "Command",
}
}
}
impl Event<'_> {
#[must_use]
pub const fn is_dialogue(&self) -> bool {
matches!(self.event_type, EventType::Dialogue)
}
#[must_use]
pub const fn is_comment(&self) -> bool {
matches!(self.event_type, EventType::Comment)
}
pub fn start_time_cs(&self) -> Result<u32, crate::utils::CoreError> {
crate::utils::parse_ass_time(self.start)
}
pub fn end_time_cs(&self) -> Result<u32, crate::utils::CoreError> {
crate::utils::parse_ass_time(self.end)
}
pub fn duration_cs(&self) -> Result<u32, crate::utils::CoreError> {
let start = self.start_time_cs()?;
let end = self.end_time_cs()?;
Ok(end.saturating_sub(start))
}
#[must_use]
pub fn to_ass_string(&self) -> alloc::string::String {
let event_type_str = self.event_type.as_str();
format!(
"{event_type_str}: {},{},{},{},{},{},{},{},{},{}",
self.layer,
self.start,
self.end,
self.style,
self.name,
self.margin_l,
self.margin_r,
self.margin_v,
self.effect,
self.text
)
}
#[must_use]
pub fn to_ass_string_with_format(&self, format: &[&str]) -> alloc::string::String {
let event_type_str = self.event_type.as_str();
let mut field_values = Vec::with_capacity(format.len());
for field in format {
let value = match *field {
"Layer" => self.layer,
"Start" => self.start,
"End" => self.end,
"Style" => self.style,
"Name" | "Actor" => self.name,
"MarginL" => self.margin_l,
"MarginR" => self.margin_r,
"MarginV" => self.margin_v,
"MarginT" => self.margin_t.unwrap_or("0"),
"MarginB" => self.margin_b.unwrap_or("0"),
"Effect" => self.effect,
"Text" => self.text,
_ => "", };
field_values.push(value);
}
format!("{event_type_str}: {}", field_values.join(","))
}
#[cfg(debug_assertions)]
#[must_use]
pub fn validate_spans(&self, source_range: &Range<usize>) -> bool {
let spans = [
self.layer,
self.start,
self.end,
self.style,
self.name,
self.margin_l,
self.margin_r,
self.margin_v,
self.effect,
self.text,
];
spans.iter().all(|span| {
let ptr = span.as_ptr() as usize;
source_range.contains(&ptr)
})
}
}
impl Default for Event<'_> {
fn default() -> Self {
Self {
event_type: EventType::Dialogue,
layer: "0",
start: "0:00:00.00",
end: "0:00:00.00",
style: "Default",
name: "",
margin_l: "0",
margin_r: "0",
margin_v: "0",
margin_t: None,
margin_b: None,
effect: "",
text: "",
span: Span::new(0, 0, 0, 0),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
use alloc::vec;
#[test]
fn event_type_parsing() {
assert_eq!(EventType::parse_type("Dialogue"), Some(EventType::Dialogue));
assert_eq!(EventType::parse_type("Comment"), Some(EventType::Comment));
assert_eq!(EventType::parse_type("Picture"), Some(EventType::Picture));
assert_eq!(EventType::parse_type("Sound"), Some(EventType::Sound));
assert_eq!(EventType::parse_type("Movie"), Some(EventType::Movie));
assert_eq!(EventType::parse_type("Command"), Some(EventType::Command));
assert_eq!(EventType::parse_type("Unknown"), None);
assert_eq!(
EventType::parse_type(" Dialogue "),
Some(EventType::Dialogue)
);
}
#[test]
fn event_type_string_conversion() {
assert_eq!(EventType::Dialogue.as_str(), "Dialogue");
assert_eq!(EventType::Comment.as_str(), "Comment");
assert_eq!(EventType::Picture.as_str(), "Picture");
assert_eq!(EventType::Sound.as_str(), "Sound");
assert_eq!(EventType::Movie.as_str(), "Movie");
assert_eq!(EventType::Command.as_str(), "Command");
}
#[test]
fn event_type_properties() {
assert_eq!(EventType::Dialogue, EventType::Dialogue);
assert_ne!(EventType::Dialogue, EventType::Comment);
}
#[test]
fn event_dialogue_check() {
let dialogue = Event {
event_type: EventType::Dialogue,
..Event::default()
};
assert!(dialogue.is_dialogue());
assert!(!dialogue.is_comment());
let comment = Event {
event_type: EventType::Comment,
..Event::default()
};
assert!(!comment.is_dialogue());
assert!(comment.is_comment());
}
#[test]
fn event_default() {
let event = Event::default();
assert_eq!(event.event_type, EventType::Dialogue);
assert_eq!(event.layer, "0");
assert_eq!(event.start, "0:00:00.00");
assert_eq!(event.end, "0:00:00.00");
assert_eq!(event.style, "Default");
assert_eq!(event.text, "");
}
#[test]
fn event_clone_eq() {
let event = Event::default();
let cloned = event.clone();
assert_eq!(event, cloned);
}
#[test]
fn event_time_parsing() {
let event = Event {
start: "0:01:30.50",
end: "0:01:35.00",
..Event::default()
};
assert_eq!(event.start_time_cs().unwrap(), 9050);
assert_eq!(event.end_time_cs().unwrap(), 9500);
assert_eq!(event.duration_cs().unwrap(), 450); }
#[test]
fn event_time_parsing_edge_cases() {
let zero_event = Event {
start: "0:00:00.00",
end: "0:00:00.00",
..Event::default()
};
assert_eq!(zero_event.start_time_cs().unwrap(), 0);
assert_eq!(zero_event.end_time_cs().unwrap(), 0);
assert_eq!(zero_event.duration_cs().unwrap(), 0);
let negative_event = Event {
start: "0:01:00.00",
end: "0:00:30.00",
..Event::default()
};
assert_eq!(negative_event.duration_cs().unwrap(), 0); }
#[test]
fn event_time_parsing_errors() {
let invalid_start = Event {
start: "invalid",
end: "0:00:05.00",
..Event::default()
};
assert!(invalid_start.start_time_cs().is_err());
assert!(invalid_start.duration_cs().is_err());
let invalid_end = Event {
start: "0:00:00.00",
end: "invalid",
..Event::default()
};
assert!(invalid_end.end_time_cs().is_err());
assert!(invalid_end.duration_cs().is_err());
}
#[test]
fn event_all_types() {
let dialogue = Event {
event_type: EventType::Dialogue,
..Event::default()
};
assert!(dialogue.is_dialogue());
assert!(!dialogue.is_comment());
let comment = Event {
event_type: EventType::Comment,
..Event::default()
};
assert!(!comment.is_dialogue());
assert!(comment.is_comment());
let picture = Event {
event_type: EventType::Picture,
..Event::default()
};
assert!(!picture.is_dialogue());
assert!(!picture.is_comment());
let sound = Event {
event_type: EventType::Sound,
..Event::default()
};
assert!(!sound.is_dialogue());
assert!(!sound.is_comment());
let movie = Event {
event_type: EventType::Movie,
..Event::default()
};
assert!(!movie.is_dialogue());
assert!(!movie.is_comment());
let command = Event {
event_type: EventType::Command,
..Event::default()
};
assert!(!command.is_dialogue());
assert!(!command.is_comment());
}
#[test]
fn event_comprehensive_creation() {
let event = Event {
event_type: EventType::Dialogue,
layer: "5",
start: "0:02:15.75",
end: "0:02:20.25",
style: "MainStyle",
name: "Character",
margin_l: "10",
margin_r: "20",
margin_v: "15",
margin_t: None,
margin_b: None,
effect: "fadeIn",
text: "Hello, world!",
span: Span::new(0, 0, 0, 0),
};
assert_eq!(event.event_type, EventType::Dialogue);
assert_eq!(event.layer, "5");
assert_eq!(event.start, "0:02:15.75");
assert_eq!(event.end, "0:02:20.25");
assert_eq!(event.style, "MainStyle");
assert_eq!(event.name, "Character");
assert_eq!(event.margin_l, "10");
assert_eq!(event.margin_r, "20");
assert_eq!(event.margin_v, "15");
assert_eq!(event.effect, "fadeIn");
assert_eq!(event.text, "Hello, world!");
}
#[test]
fn event_debug_output() {
let event = Event {
event_type: EventType::Dialogue,
text: "Test text",
..Event::default()
};
let debug_str = format!("{event:?}");
assert!(debug_str.contains("Event"));
assert!(debug_str.contains("Dialogue"));
assert!(debug_str.contains("Test text"));
}
#[test]
fn event_equality() {
let event1 = Event {
event_type: EventType::Dialogue,
text: "Same text",
..Event::default()
};
let event2 = Event {
event_type: EventType::Dialogue,
text: "Same text",
..Event::default()
};
assert_eq!(event1, event2);
let event3 = Event {
event_type: EventType::Comment,
text: "Same text",
..Event::default()
};
assert_ne!(event1, event3);
}
#[cfg(debug_assertions)]
#[test]
fn event_validate_spans() {
let source = "Dialogue,0,0:00:05.00,0:00:10.00,Default,Character,0,0,0,,Hello world";
let source_start = source.as_ptr() as usize;
let source_end = source_start + source.len();
let source_range = source_start..source_end;
let fields: Vec<&str> = source.split(',').collect();
let event = Event {
event_type: EventType::Dialogue,
layer: fields[1],
start: fields[2],
end: fields[3],
style: fields[4],
name: fields[5],
margin_l: fields[6],
margin_r: fields[7],
margin_v: fields[8],
margin_t: None,
margin_b: None,
effect: fields[9],
text: fields[10],
span: Span::new(0, 0, 0, 0),
};
assert!(event.validate_spans(&source_range));
assert_eq!(event.layer, "0");
assert_eq!(event.start, "0:00:05.00");
assert_eq!(event.end, "0:00:10.00");
assert_eq!(event.style, "Default");
assert_eq!(event.name, "Character");
assert_eq!(event.text, "Hello world");
}
#[cfg(debug_assertions)]
#[test]
fn event_validate_spans_invalid() {
let source1 = "Dialogue,0,0:00:05.00,0:00:10.00,Default";
let source2 = "Other,Character,Hello";
let source1_start = source1.as_ptr() as usize;
let source1_end = source1_start + source1.len();
let source1_range = source1_start..source1_end;
let event = Event {
event_type: EventType::Dialogue,
layer: "0",
start: "0:00:05.00",
end: "0:00:10.00",
style: "Default",
name: &source2[6..15], text: &source2[16..21], ..Event::default()
};
assert!(!event.validate_spans(&source1_range));
}
#[test]
fn event_type_parse_edge_cases() {
assert_eq!(EventType::parse_type("dialogue"), None);
assert_eq!(EventType::parse_type("DIALOGUE"), None);
assert_eq!(EventType::parse_type(""), None);
assert_eq!(EventType::parse_type(" "), None);
assert_eq!(
EventType::parse_type(" Comment "),
Some(EventType::Comment)
);
assert_eq!(
EventType::parse_type("\tPicture\n"),
Some(EventType::Picture)
);
}
#[test]
fn event_mixed_defaults() {
let event = Event {
event_type: EventType::Picture,
start: "0:01:00.00",
text: "Custom text",
..Event::default()
};
assert_eq!(event.event_type, EventType::Picture);
assert_eq!(event.start, "0:01:00.00");
assert_eq!(event.text, "Custom text");
assert_eq!(event.layer, "0");
assert_eq!(event.end, "0:00:00.00");
assert_eq!(event.style, "Default");
assert_eq!(event.name, "");
assert_eq!(event.effect, "");
}
#[test]
fn event_to_ass_string() {
let event = Event {
event_type: EventType::Dialogue,
layer: "0",
start: "0:00:05.00",
end: "0:00:10.00",
style: "Default",
name: "Speaker",
margin_l: "10",
margin_r: "20",
margin_v: "15",
effect: "fade",
text: "Hello world",
..Event::default()
};
let ass_string = event.to_ass_string();
assert_eq!(
ass_string,
"Dialogue: 0,0:00:05.00,0:00:10.00,Default,Speaker,10,20,15,fade,Hello world"
);
}
#[test]
fn event_to_ass_string_with_format() {
let event = Event {
event_type: EventType::Comment,
start: "0:00:00.00",
end: "0:00:05.00",
text: "Test comment",
..Event::default()
};
let v4_format = vec![
"Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV", "Effect",
"Text",
];
let v4_string = event.to_ass_string_with_format(&v4_format);
assert_eq!(
v4_string,
"Comment: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Test comment"
);
let min_format = vec!["Start", "End", "Text"];
let min_string = event.to_ass_string_with_format(&min_format);
assert_eq!(min_string, "Comment: 0:00:00.00,0:00:05.00,Test comment");
let event_v4pp = Event {
event_type: EventType::Dialogue,
margin_t: Some("5"),
margin_b: Some("10"),
text: "V4++ test",
..Event::default()
};
let v4pp_format = vec![
"Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginT", "MarginB",
"Effect", "Text",
];
let v4pp_string = event_v4pp.to_ass_string_with_format(&v4pp_format);
assert_eq!(
v4pp_string,
"Dialogue: 0,0:00:00.00,0:00:00.00,Default,,0,0,5,10,,V4++ test"
);
}
}