1#[cfg(not(feature = "std"))]
7extern crate alloc;
8#[cfg(not(feature = "std"))]
9use alloc::{format, vec::Vec};
10
11use super::Span;
12#[cfg(debug_assertions)]
13use core::ops::Range;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Event<'a> {
38 pub event_type: EventType,
40
41 pub layer: &'a str,
43
44 pub start: &'a str,
46
47 pub end: &'a str,
49
50 pub style: &'a str,
52
53 pub name: &'a str,
55
56 pub margin_l: &'a str,
58
59 pub margin_r: &'a str,
61
62 pub margin_v: &'a str,
64
65 pub margin_t: Option<&'a str>,
67
68 pub margin_b: Option<&'a str>,
70
71 pub effect: &'a str,
73
74 pub text: &'a str,
76
77 pub span: Span,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub enum EventType {
87 Dialogue,
89
90 Comment,
92
93 Picture,
95
96 Sound,
98
99 Movie,
101
102 Command,
104}
105
106impl EventType {
107 #[must_use]
121 pub fn parse_type(s: &str) -> Option<Self> {
122 match s.trim() {
123 "Dialogue" => Some(Self::Dialogue),
124 "Comment" => Some(Self::Comment),
125 "Picture" => Some(Self::Picture),
126 "Sound" => Some(Self::Sound),
127 "Movie" => Some(Self::Movie),
128 "Command" => Some(Self::Command),
129 _ => None,
130 }
131 }
132
133 #[must_use]
137 pub const fn as_str(self) -> &'static str {
138 match self {
139 Self::Dialogue => "Dialogue",
140 Self::Comment => "Comment",
141 Self::Picture => "Picture",
142 Self::Sound => "Sound",
143 Self::Movie => "Movie",
144 Self::Command => "Command",
145 }
146 }
147}
148
149impl Event<'_> {
150 #[must_use]
154 pub const fn is_dialogue(&self) -> bool {
155 matches!(self.event_type, EventType::Dialogue)
156 }
157
158 #[must_use]
162 pub const fn is_comment(&self) -> bool {
163 matches!(self.event_type, EventType::Comment)
164 }
165
166 pub fn start_time_cs(&self) -> Result<u32, crate::utils::CoreError> {
175 crate::utils::parse_ass_time(self.start)
176 }
177
178 pub fn end_time_cs(&self) -> Result<u32, crate::utils::CoreError> {
187 crate::utils::parse_ass_time(self.end)
188 }
189
190 pub fn duration_cs(&self) -> Result<u32, crate::utils::CoreError> {
199 let start = self.start_time_cs()?;
200 let end = self.end_time_cs()?;
201 Ok(end.saturating_sub(start))
202 }
203
204 #[must_use]
228 pub fn to_ass_string(&self) -> alloc::string::String {
229 let event_type_str = self.event_type.as_str();
230
231 format!(
234 "{event_type_str}: {},{},{},{},{},{},{},{},{},{}",
235 self.layer,
236 self.start,
237 self.end,
238 self.style,
239 self.name,
240 self.margin_l,
241 self.margin_r,
242 self.margin_v,
243 self.effect,
244 self.text
245 )
246 }
247
248 #[must_use]
275 pub fn to_ass_string_with_format(&self, format: &[&str]) -> alloc::string::String {
276 let event_type_str = self.event_type.as_str();
277 let mut field_values = Vec::with_capacity(format.len());
278
279 for field in format {
280 let value = match *field {
281 "Layer" => self.layer,
282 "Start" => self.start,
283 "End" => self.end,
284 "Style" => self.style,
285 "Name" | "Actor" => self.name,
286 "MarginL" => self.margin_l,
287 "MarginR" => self.margin_r,
288 "MarginV" => self.margin_v,
289 "MarginT" => self.margin_t.unwrap_or("0"),
290 "MarginB" => self.margin_b.unwrap_or("0"),
291 "Effect" => self.effect,
292 "Text" => self.text,
293 _ => "", };
295 field_values.push(value);
296 }
297
298 format!("{event_type_str}: {}", field_values.join(","))
299 }
300
301 #[cfg(debug_assertions)]
309 #[must_use]
310 pub fn validate_spans(&self, source_range: &Range<usize>) -> bool {
311 let spans = [
312 self.layer,
313 self.start,
314 self.end,
315 self.style,
316 self.name,
317 self.margin_l,
318 self.margin_r,
319 self.margin_v,
320 self.effect,
321 self.text,
322 ];
323
324 spans.iter().all(|span| {
325 let ptr = span.as_ptr() as usize;
326 source_range.contains(&ptr)
327 })
328 }
329}
330
331impl Default for Event<'_> {
332 fn default() -> Self {
337 Self {
338 event_type: EventType::Dialogue,
339 layer: "0",
340 start: "0:00:00.00",
341 end: "0:00:00.00",
342 style: "Default",
343 name: "",
344 margin_l: "0",
345 margin_r: "0",
346 margin_v: "0",
347 margin_t: None,
348 margin_b: None,
349 effect: "",
350 text: "",
351 span: Span::new(0, 0, 0, 0),
352 }
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 #[cfg(not(feature = "std"))]
360 use alloc::vec;
361
362 #[test]
363 fn event_type_parsing() {
364 assert_eq!(EventType::parse_type("Dialogue"), Some(EventType::Dialogue));
365 assert_eq!(EventType::parse_type("Comment"), Some(EventType::Comment));
366 assert_eq!(EventType::parse_type("Picture"), Some(EventType::Picture));
367 assert_eq!(EventType::parse_type("Sound"), Some(EventType::Sound));
368 assert_eq!(EventType::parse_type("Movie"), Some(EventType::Movie));
369 assert_eq!(EventType::parse_type("Command"), Some(EventType::Command));
370 assert_eq!(EventType::parse_type("Unknown"), None);
371 assert_eq!(
372 EventType::parse_type(" Dialogue "),
373 Some(EventType::Dialogue)
374 );
375 }
376
377 #[test]
378 fn event_type_string_conversion() {
379 assert_eq!(EventType::Dialogue.as_str(), "Dialogue");
380 assert_eq!(EventType::Comment.as_str(), "Comment");
381 assert_eq!(EventType::Picture.as_str(), "Picture");
382 assert_eq!(EventType::Sound.as_str(), "Sound");
383 assert_eq!(EventType::Movie.as_str(), "Movie");
384 assert_eq!(EventType::Command.as_str(), "Command");
385 }
386
387 #[test]
388 fn event_type_properties() {
389 assert_eq!(EventType::Dialogue, EventType::Dialogue);
390 assert_ne!(EventType::Dialogue, EventType::Comment);
391 }
392
393 #[test]
394 fn event_dialogue_check() {
395 let dialogue = Event {
396 event_type: EventType::Dialogue,
397 ..Event::default()
398 };
399 assert!(dialogue.is_dialogue());
400 assert!(!dialogue.is_comment());
401
402 let comment = Event {
403 event_type: EventType::Comment,
404 ..Event::default()
405 };
406 assert!(!comment.is_dialogue());
407 assert!(comment.is_comment());
408 }
409
410 #[test]
411 fn event_default() {
412 let event = Event::default();
413 assert_eq!(event.event_type, EventType::Dialogue);
414 assert_eq!(event.layer, "0");
415 assert_eq!(event.start, "0:00:00.00");
416 assert_eq!(event.end, "0:00:00.00");
417 assert_eq!(event.style, "Default");
418 assert_eq!(event.text, "");
419 }
420
421 #[test]
422 fn event_clone_eq() {
423 let event = Event::default();
424 let cloned = event.clone();
425 assert_eq!(event, cloned);
426 }
427
428 #[test]
429 fn event_time_parsing() {
430 let event = Event {
431 start: "0:01:30.50",
432 end: "0:01:35.00",
433 ..Event::default()
434 };
435
436 assert_eq!(event.start_time_cs().unwrap(), 9050); assert_eq!(event.end_time_cs().unwrap(), 9500); assert_eq!(event.duration_cs().unwrap(), 450); }
445
446 #[test]
447 fn event_time_parsing_edge_cases() {
448 let zero_event = Event {
450 start: "0:00:00.00",
451 end: "0:00:00.00",
452 ..Event::default()
453 };
454 assert_eq!(zero_event.start_time_cs().unwrap(), 0);
455 assert_eq!(zero_event.end_time_cs().unwrap(), 0);
456 assert_eq!(zero_event.duration_cs().unwrap(), 0);
457
458 let negative_event = Event {
460 start: "0:01:00.00",
461 end: "0:00:30.00",
462 ..Event::default()
463 };
464 assert_eq!(negative_event.duration_cs().unwrap(), 0); }
466
467 #[test]
468 fn event_time_parsing_errors() {
469 let invalid_start = Event {
471 start: "invalid",
472 end: "0:00:05.00",
473 ..Event::default()
474 };
475 assert!(invalid_start.start_time_cs().is_err());
476 assert!(invalid_start.duration_cs().is_err());
477
478 let invalid_end = Event {
479 start: "0:00:00.00",
480 end: "invalid",
481 ..Event::default()
482 };
483 assert!(invalid_end.end_time_cs().is_err());
484 assert!(invalid_end.duration_cs().is_err());
485 }
486
487 #[test]
488 fn event_all_types() {
489 let dialogue = Event {
491 event_type: EventType::Dialogue,
492 ..Event::default()
493 };
494 assert!(dialogue.is_dialogue());
495 assert!(!dialogue.is_comment());
496
497 let comment = Event {
498 event_type: EventType::Comment,
499 ..Event::default()
500 };
501 assert!(!comment.is_dialogue());
502 assert!(comment.is_comment());
503
504 let picture = Event {
505 event_type: EventType::Picture,
506 ..Event::default()
507 };
508 assert!(!picture.is_dialogue());
509 assert!(!picture.is_comment());
510
511 let sound = Event {
512 event_type: EventType::Sound,
513 ..Event::default()
514 };
515 assert!(!sound.is_dialogue());
516 assert!(!sound.is_comment());
517
518 let movie = Event {
519 event_type: EventType::Movie,
520 ..Event::default()
521 };
522 assert!(!movie.is_dialogue());
523 assert!(!movie.is_comment());
524
525 let command = Event {
526 event_type: EventType::Command,
527 ..Event::default()
528 };
529 assert!(!command.is_dialogue());
530 assert!(!command.is_comment());
531 }
532
533 #[test]
534 fn event_comprehensive_creation() {
535 let event = Event {
536 event_type: EventType::Dialogue,
537 layer: "5",
538 start: "0:02:15.75",
539 end: "0:02:20.25",
540 style: "MainStyle",
541 name: "Character",
542 margin_l: "10",
543 margin_r: "20",
544 margin_v: "15",
545 margin_t: None,
546 margin_b: None,
547 effect: "fadeIn",
548 text: "Hello, world!",
549 span: Span::new(0, 0, 0, 0),
550 };
551
552 assert_eq!(event.event_type, EventType::Dialogue);
553 assert_eq!(event.layer, "5");
554 assert_eq!(event.start, "0:02:15.75");
555 assert_eq!(event.end, "0:02:20.25");
556 assert_eq!(event.style, "MainStyle");
557 assert_eq!(event.name, "Character");
558 assert_eq!(event.margin_l, "10");
559 assert_eq!(event.margin_r, "20");
560 assert_eq!(event.margin_v, "15");
561 assert_eq!(event.effect, "fadeIn");
562 assert_eq!(event.text, "Hello, world!");
563 }
564
565 #[test]
566 fn event_debug_output() {
567 let event = Event {
568 event_type: EventType::Dialogue,
569 text: "Test text",
570 ..Event::default()
571 };
572
573 let debug_str = format!("{event:?}");
574 assert!(debug_str.contains("Event"));
575 assert!(debug_str.contains("Dialogue"));
576 assert!(debug_str.contains("Test text"));
577 }
578
579 #[test]
580 fn event_equality() {
581 let event1 = Event {
582 event_type: EventType::Dialogue,
583 text: "Same text",
584 ..Event::default()
585 };
586
587 let event2 = Event {
588 event_type: EventType::Dialogue,
589 text: "Same text",
590 ..Event::default()
591 };
592
593 assert_eq!(event1, event2);
594
595 let event3 = Event {
596 event_type: EventType::Comment,
597 text: "Same text",
598 ..Event::default()
599 };
600
601 assert_ne!(event1, event3);
602 }
603
604 #[cfg(debug_assertions)]
605 #[test]
606 fn event_validate_spans() {
607 let source = "Dialogue,0,0:00:05.00,0:00:10.00,Default,Character,0,0,0,,Hello world";
608 let source_start = source.as_ptr() as usize;
609 let source_end = source_start + source.len();
610 let source_range = source_start..source_end;
611
612 let fields: Vec<&str> = source.split(',').collect();
613 let event = Event {
614 event_type: EventType::Dialogue,
615 layer: fields[1],
616 start: fields[2],
617 end: fields[3],
618 style: fields[4],
619 name: fields[5],
620 margin_l: fields[6],
621 margin_r: fields[7],
622 margin_v: fields[8],
623 margin_t: None,
624 margin_b: None,
625 effect: fields[9],
626 text: fields[10],
627 span: Span::new(0, 0, 0, 0),
628 };
629
630 assert!(event.validate_spans(&source_range));
631 assert_eq!(event.layer, "0");
632 assert_eq!(event.start, "0:00:05.00");
633 assert_eq!(event.end, "0:00:10.00");
634 assert_eq!(event.style, "Default");
635 assert_eq!(event.name, "Character");
636 assert_eq!(event.text, "Hello world");
637 }
638
639 #[cfg(debug_assertions)]
640 #[test]
641 fn event_validate_spans_invalid() {
642 let source1 = "Dialogue,0,0:00:05.00,0:00:10.00,Default";
643 let source2 = "Other,Character,Hello";
644 let source1_start = source1.as_ptr() as usize;
645 let source1_end = source1_start + source1.len();
646 let source1_range = source1_start..source1_end;
647
648 let event = Event {
649 event_type: EventType::Dialogue,
650 layer: "0",
651 start: "0:00:05.00",
652 end: "0:00:10.00",
653 style: "Default",
654 name: &source2[6..15], text: &source2[16..21], ..Event::default()
657 };
658
659 assert!(!event.validate_spans(&source1_range));
661 }
662
663 #[test]
664 fn event_type_parse_edge_cases() {
665 assert_eq!(EventType::parse_type("dialogue"), None);
667 assert_eq!(EventType::parse_type("DIALOGUE"), None);
668
669 assert_eq!(EventType::parse_type(""), None);
671 assert_eq!(EventType::parse_type(" "), None);
672
673 assert_eq!(
675 EventType::parse_type(" Comment "),
676 Some(EventType::Comment)
677 );
678 assert_eq!(
679 EventType::parse_type("\tPicture\n"),
680 Some(EventType::Picture)
681 );
682 }
683
684 #[test]
685 fn event_mixed_defaults() {
686 let event = Event {
687 event_type: EventType::Picture,
688 start: "0:01:00.00",
689 text: "Custom text",
690 ..Event::default()
691 };
692
693 assert_eq!(event.event_type, EventType::Picture);
695 assert_eq!(event.start, "0:01:00.00");
696 assert_eq!(event.text, "Custom text");
697
698 assert_eq!(event.layer, "0");
700 assert_eq!(event.end, "0:00:00.00");
701 assert_eq!(event.style, "Default");
702 assert_eq!(event.name, "");
703 assert_eq!(event.effect, "");
704 }
705
706 #[test]
707 fn event_to_ass_string() {
708 let event = Event {
709 event_type: EventType::Dialogue,
710 layer: "0",
711 start: "0:00:05.00",
712 end: "0:00:10.00",
713 style: "Default",
714 name: "Speaker",
715 margin_l: "10",
716 margin_r: "20",
717 margin_v: "15",
718 effect: "fade",
719 text: "Hello world",
720 ..Event::default()
721 };
722
723 let ass_string = event.to_ass_string();
724 assert_eq!(
725 ass_string,
726 "Dialogue: 0,0:00:05.00,0:00:10.00,Default,Speaker,10,20,15,fade,Hello world"
727 );
728 }
729
730 #[test]
731 fn event_to_ass_string_with_format() {
732 let event = Event {
733 event_type: EventType::Comment,
734 start: "0:00:00.00",
735 end: "0:00:05.00",
736 text: "Test comment",
737 ..Event::default()
738 };
739
740 let v4_format = vec![
742 "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV", "Effect",
743 "Text",
744 ];
745 let v4_string = event.to_ass_string_with_format(&v4_format);
746 assert_eq!(
747 v4_string,
748 "Comment: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Test comment"
749 );
750
751 let min_format = vec!["Start", "End", "Text"];
753 let min_string = event.to_ass_string_with_format(&min_format);
754 assert_eq!(min_string, "Comment: 0:00:00.00,0:00:05.00,Test comment");
755
756 let event_v4pp = Event {
758 event_type: EventType::Dialogue,
759 margin_t: Some("5"),
760 margin_b: Some("10"),
761 text: "V4++ test",
762 ..Event::default()
763 };
764 let v4pp_format = vec![
765 "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginT", "MarginB",
766 "Effect", "Text",
767 ];
768 let v4pp_string = event_v4pp.to_ass_string_with_format(&v4pp_format);
769 assert_eq!(
770 v4pp_string,
771 "Dialogue: 0,0:00:00.00,0:00:00.00,Default,,0,0,5,10,,V4++ test"
772 );
773 }
774}