ass_core/parser/ast/
mod.rs

1//! AST (Abstract Syntax Tree) definitions for ASS scripts
2//!
3//! Provides zero-copy AST nodes using lifetime-generic design for maximum performance.
4//! All nodes reference spans in the original source text to avoid allocations.
5//!
6//! # Thread Safety
7//!
8//! All AST nodes are immutable after construction and implement `Send + Sync`
9//! for safe multi-threaded access.
10//!
11//! # Performance
12//!
13//! - Zero allocations via `&'a str` spans
14//! - Memory usage ~1.1x input size
15//! - Validation via pointer arithmetic for span checking
16//!
17//! # Examples
18//!
19//! ```rust
20//! use ass_core::parser::ast::{Section, ScriptInfo, Event, EventType, Span};
21//!
22//! // Create script info
23//! let info = ScriptInfo { fields: vec![("Title", "Test")], span: Span::new(0, 0, 0, 0) };
24//! let section = Section::ScriptInfo(info);
25//!
26//! // Create dialogue event
27//! let event = Event {
28//!     event_type: EventType::Dialogue,
29//!     start: "0:00:05.00",
30//!     end: "0:00:10.00",
31//!     text: "Hello World!",
32//!     ..Event::default()
33//! };
34//! ```
35
36#[cfg(not(feature = "std"))]
37extern crate alloc;
38
39mod event;
40mod media;
41mod script_info;
42mod section;
43mod style;
44// Re-export all public types to maintain API compatibility
45pub use event::{Event, EventType};
46pub use media::{Font, Graphic};
47pub use script_info::ScriptInfo;
48pub use section::{Section, SectionType};
49pub use style::Style;
50
51/// Represents a span in the source text with position information
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct Span {
54    /// Byte offset in source where span starts
55    pub start: usize,
56    /// Byte offset in source where span ends
57    pub end: usize,
58    /// Line number (1-based) where span starts
59    pub line: u32,
60    /// Column number (1-based) where span starts
61    pub column: u32,
62}
63
64impl Span {
65    /// Create a new span with position information
66    #[must_use]
67    pub const fn new(start: usize, end: usize, line: u32, column: u32) -> Self {
68        Self {
69            start,
70            end,
71            line,
72            column,
73        }
74    }
75
76    /// Check if a byte offset is contained within this span
77    #[must_use]
78    pub const fn contains(&self, offset: usize) -> bool {
79        offset >= self.start && offset < self.end
80    }
81
82    /// Merge two spans to create a span covering both
83    #[must_use]
84    pub fn merge(&self, other: &Self) -> Self {
85        use core::cmp::Ordering;
86
87        Self {
88            start: self.start.min(other.start),
89            end: self.end.max(other.end),
90            line: self.line.min(other.line),
91            column: match self.line.cmp(&other.line) {
92                Ordering::Less => self.column,
93                Ordering::Greater => other.column,
94                Ordering::Equal => self.column.min(other.column),
95            },
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    #[cfg(not(feature = "std"))]
104    use alloc::vec;
105
106    #[test]
107    fn test_span_creation() {
108        let span = Span::new(0, 10, 1, 1);
109        assert_eq!(span.start, 0);
110        assert_eq!(span.end, 10);
111        assert_eq!(span.line, 1);
112        assert_eq!(span.column, 1);
113    }
114
115    #[test]
116    fn test_span_contains() {
117        let span = Span::new(0, 10, 1, 1);
118        assert!(span.contains(0));
119        assert!(span.contains(5));
120        assert!(span.contains(9));
121        assert!(!span.contains(10));
122        assert!(!span.contains(15));
123    }
124
125    #[test]
126    fn test_span_merge() {
127        let span1 = Span::new(0, 10, 1, 1);
128        let span2 = Span::new(5, 15, 1, 6);
129        let merged = span1.merge(&span2);
130
131        assert_eq!(merged.start, 0);
132        assert_eq!(merged.end, 15);
133        assert_eq!(merged.line, 1);
134        assert_eq!(merged.column, 1);
135
136        // Test merge with different lines
137        let span3 = Span::new(20, 30, 2, 5);
138        let span4 = Span::new(25, 35, 3, 10);
139        let merged2 = span3.merge(&span4);
140
141        assert_eq!(merged2.start, 20);
142        assert_eq!(merged2.end, 35);
143        assert_eq!(merged2.line, 2);
144        assert_eq!(merged2.column, 5);
145    }
146
147    #[test]
148    fn ast_integration_script_info() {
149        let fields = vec![("Title", "Integration Test"), ("ScriptType", "v4.00+")];
150        let info = ScriptInfo {
151            fields,
152            span: Span::new(0, 0, 0, 0),
153        };
154        let section = Section::ScriptInfo(info);
155
156        assert_eq!(section.section_type(), SectionType::ScriptInfo);
157    }
158
159    #[test]
160    fn ast_integration_events() {
161        let event = Event {
162            event_type: EventType::Dialogue,
163            start: "0:00:05.00",
164            end: "0:00:10.00",
165            style: "Default",
166            text: "Test dialogue",
167            ..Event::default()
168        };
169
170        let events = vec![event];
171        let section = Section::Events(events);
172
173        assert_eq!(section.section_type(), SectionType::Events);
174    }
175
176    #[test]
177    fn ast_integration_styles() {
178        let style = Style {
179            name: "TestStyle",
180            fontname: "Arial",
181            fontsize: "20",
182            ..Style::default()
183        };
184
185        let styles = vec![style];
186        let section = Section::Styles(styles);
187
188        assert_eq!(section.section_type(), SectionType::Styles);
189    }
190
191    #[test]
192    fn ast_integration_fonts() {
193        let font = Font {
194            filename: "test.ttf",
195            data_lines: vec!["encoded data line 1", "encoded data line 2"],
196            span: Span::new(0, 0, 0, 0),
197        };
198
199        let fonts = vec![font];
200        let section = Section::Fonts(fonts);
201
202        assert_eq!(section.section_type(), SectionType::Fonts);
203    }
204
205    #[test]
206    fn ast_integration_graphics() {
207        let graphic = Graphic {
208            filename: "logo.png",
209            data_lines: vec!["encoded image data"],
210            span: Span::new(0, 0, 0, 0),
211        };
212
213        let graphics = vec![graphic];
214        let section = Section::Graphics(graphics);
215
216        assert_eq!(section.section_type(), SectionType::Graphics);
217    }
218
219    #[test]
220    fn event_type_round_trip() {
221        let types = [
222            EventType::Dialogue,
223            EventType::Comment,
224            EventType::Picture,
225            EventType::Sound,
226            EventType::Movie,
227            EventType::Command,
228        ];
229
230        for event_type in types {
231            let str_repr = event_type.as_str();
232            let parsed = EventType::parse_type(str_repr);
233            assert_eq!(parsed, Some(event_type));
234        }
235    }
236
237    #[test]
238    fn section_type_properties() {
239        assert!(SectionType::ScriptInfo.is_required());
240        assert!(SectionType::Events.is_required());
241        assert!(!SectionType::Styles.is_required());
242
243        assert!(SectionType::Events.is_timed());
244        assert!(!SectionType::ScriptInfo.is_timed());
245
246        assert_eq!(SectionType::ScriptInfo.header_name(), "Script Info");
247        assert_eq!(SectionType::Events.header_name(), "Events");
248    }
249}