ass_core/parser/ast/
event.rs

1//! Event AST node for ASS dialogue and commands
2//!
3//! Contains the Event struct and `EventType` enum representing events from the
4//! [Events] section with zero-copy design and time parsing utilities.
5
6#[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/// Event from `[Events\]` section (dialogue, comments, etc.)
16///
17/// Represents a single event in the subtitle timeline. Events can be dialogue
18/// lines, comments, or other commands with associated timing and styling.
19/// All fields use zero-copy string references for maximum efficiency.
20///
21/// # Examples
22///
23/// ```rust
24/// use ass_core::parser::ast::{Event, EventType};
25///
26/// let event = Event {
27///     event_type: EventType::Dialogue,
28///     start: "0:00:05.00",
29///     end: "0:00:10.00",
30///     text: "Hello, world!",
31///     ..Event::default()
32/// };
33///
34/// assert!(event.is_dialogue());
35/// ```
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Event<'a> {
38    /// Event type (Dialogue, Comment, etc.)
39    pub event_type: EventType,
40
41    /// Layer for drawing order (higher layers drawn on top)
42    pub layer: &'a str,
43
44    /// Start time in ASS time format (H:MM:SS.CS)
45    pub start: &'a str,
46
47    /// End time in ASS time format (H:MM:SS.CS)
48    pub end: &'a str,
49
50    /// Style name reference
51    pub style: &'a str,
52
53    /// Character name or speaker
54    pub name: &'a str,
55
56    /// Left margin override (pixels)
57    pub margin_l: &'a str,
58
59    /// Right margin override (pixels)
60    pub margin_r: &'a str,
61
62    /// Vertical margin override (pixels) (V4+)
63    pub margin_v: &'a str,
64
65    /// Top margin override (pixels) (V4++)
66    pub margin_t: Option<&'a str>,
67
68    /// Bottom margin override (pixels) (V4++)
69    pub margin_b: Option<&'a str>,
70
71    /// Effect specification for special rendering
72    pub effect: &'a str,
73
74    /// Text content with possible style overrides
75    pub text: &'a str,
76
77    /// Span in source text where this event is defined
78    pub span: Span,
79}
80
81/// Event type discriminant for different kinds of timeline events
82///
83/// Determines how the event is processed during subtitle rendering.
84/// Different types have different behaviors during playback.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub enum EventType {
87    /// Dialogue line (displayed during playback)
88    Dialogue,
89
90    /// Comment (ignored during playback)
91    Comment,
92
93    /// Picture display event
94    Picture,
95
96    /// Sound playback event
97    Sound,
98
99    /// Movie playback event
100    Movie,
101
102    /// Command execution event
103    Command,
104}
105
106impl EventType {
107    /// Parse event type from string
108    ///
109    /// Converts ASS event type names to the corresponding enum variant.
110    /// Returns `None` for unrecognized event types.
111    ///
112    /// # Examples
113    ///
114    /// ```rust
115    /// # use ass_core::parser::ast::EventType;
116    /// assert_eq!(EventType::parse_type("Dialogue"), Some(EventType::Dialogue));
117    /// assert_eq!(EventType::parse_type("Comment"), Some(EventType::Comment));
118    /// assert_eq!(EventType::parse_type("Unknown"), None);
119    /// ```
120    #[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    /// Get string representation for serialization
134    ///
135    /// Returns the canonical ASS event type name for this variant.
136    #[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    /// Check if this is a dialogue event
151    ///
152    /// Returns `true` for events that should be displayed during playback.
153    #[must_use]
154    pub const fn is_dialogue(&self) -> bool {
155        matches!(self.event_type, EventType::Dialogue)
156    }
157
158    /// Check if this is a comment event
159    ///
160    /// Returns `true` for events that are comments and not displayed.
161    #[must_use]
162    pub const fn is_comment(&self) -> bool {
163        matches!(self.event_type, EventType::Comment)
164    }
165
166    /// Parse start time to centiseconds
167    ///
168    /// Converts the start time string to centiseconds for timing calculations.
169    /// Uses the standard ASS time format parser.
170    ///
171    /// # Errors
172    ///
173    /// Returns an error if the time format is invalid or cannot be parsed.
174    pub fn start_time_cs(&self) -> Result<u32, crate::utils::CoreError> {
175        crate::utils::parse_ass_time(self.start)
176    }
177
178    /// Parse end time to centiseconds
179    ///
180    /// Converts the end time string to centiseconds for timing calculations.
181    /// Uses the standard ASS time format parser.
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if the time format is invalid or cannot be parsed.
186    pub fn end_time_cs(&self) -> Result<u32, crate::utils::CoreError> {
187        crate::utils::parse_ass_time(self.end)
188    }
189
190    /// Get duration in centiseconds
191    ///
192    /// Calculates the event duration by subtracting start time from end time.
193    /// Returns 0 if start time is greater than end time.
194    ///
195    /// # Errors
196    ///
197    /// Returns an error if either start or end time format is invalid.
198    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    /// Convert event to ASS string representation
205    ///
206    /// Generates the standard ASS event line format. Uses `margin_v` by default,
207    /// but will use `margin_t/margin_b` if provided (V4++ format).
208    ///
209    /// # Examples
210    ///
211    /// ```rust
212    /// # use ass_core::parser::ast::{Event, EventType};
213    /// let event = Event {
214    ///     event_type: EventType::Dialogue,
215    ///     layer: "0",
216    ///     start: "0:00:05.00",
217    ///     end: "0:00:10.00",
218    ///     style: "Default",
219    ///     text: "Hello",
220    ///     ..Event::default()
221    /// };
222    /// assert_eq!(
223    ///     event.to_ass_string(),
224    ///     "Dialogue: 0,0:00:05.00,0:00:10.00,Default,,0,0,0,,Hello"
225    /// );
226    /// ```
227    #[must_use]
228    pub fn to_ass_string(&self) -> alloc::string::String {
229        let event_type_str = self.event_type.as_str();
230
231        // Use standard V4+ format by default
232        // TODO: Support custom format lines
233        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    /// Convert event to ASS string with specific format
249    ///
250    /// Generates an ASS event line according to the provided format specification.
251    /// This allows handling both V4+ and V4++ formats, as well as custom formats.
252    ///
253    /// # Arguments
254    ///
255    /// * `format` - Field names in order (e.g., `["Layer", "Start", "End", "Style", "Text"]`)
256    ///
257    /// # Examples
258    ///
259    /// ```rust
260    /// # use ass_core::parser::ast::{Event, EventType};
261    /// let event = Event {
262    ///     event_type: EventType::Comment,
263    ///     start: "0:00:00.00",
264    ///     end: "0:00:05.00",
265    ///     text: "Note",
266    ///     ..Event::default()
267    /// };
268    /// let format = vec!["Start", "End", "Text"];
269    /// assert_eq!(
270    ///     event.to_ass_string_with_format(&format),
271    ///     "Comment: 0:00:00.00,0:00:05.00,Note"
272    /// );
273    /// ```
274    #[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                _ => "", // Unknown fields default to empty
294            };
295            field_values.push(value);
296        }
297
298        format!("{event_type_str}: {}", field_values.join(","))
299    }
300
301    /// Validate all spans in this Event reference valid source
302    ///
303    /// Debug helper to ensure zero-copy invariants are maintained.
304    /// Validates that all string references point to memory within
305    /// the specified source range.
306    ///
307    /// Only available in debug builds to avoid performance overhead.
308    #[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    /// Create default event with safe initial values
333    ///
334    /// Provides a valid default event that can be used as a template
335    /// or for testing purposes.
336    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        // Test start time parsing
437        assert_eq!(event.start_time_cs().unwrap(), 9050); // 1*60*100 + 30*100 + 50
438
439        // Test end time parsing
440        assert_eq!(event.end_time_cs().unwrap(), 9500); // 1*60*100 + 35*100
441
442        // Test duration calculation
443        assert_eq!(event.duration_cs().unwrap(), 450); // 9500 - 9050
444    }
445
446    #[test]
447    fn event_time_parsing_edge_cases() {
448        // Test zero time
449        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        // Test negative duration (end before start)
459        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); // saturating_sub returns 0
465    }
466
467    #[test]
468    fn event_time_parsing_errors() {
469        // Test invalid time formats
470        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        // Test all event types
490        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],  // "Character" from different source
655            text: &source2[16..21], // "Hello" from different source
656            ..Event::default()
657        };
658
659        // Should fail since some fields are from different source
660        assert!(!event.validate_spans(&source1_range));
661    }
662
663    #[test]
664    fn event_type_parse_edge_cases() {
665        // Test case sensitivity
666        assert_eq!(EventType::parse_type("dialogue"), None);
667        assert_eq!(EventType::parse_type("DIALOGUE"), None);
668
669        // Test empty and whitespace
670        assert_eq!(EventType::parse_type(""), None);
671        assert_eq!(EventType::parse_type("   "), None);
672
673        // Test with extra whitespace
674        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        // Custom fields
694        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        // Default fields
699        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        // V4+ format
741        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        // Custom minimal format
752        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        // V4++ format with margin_t/margin_b
757        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}