ass_core/parser/ast/
section.rs

1//! AST section types and validation for ASS scripts
2//!
3//! Defines the top-level Section enum that represents the main sections
4//! of an ASS script ([Script Info], [V4+ Styles], [Events], etc.) with
5//! zero-copy design and span validation for debugging.
6
7use alloc::vec::Vec;
8
9#[cfg(not(feature = "std"))]
10extern crate alloc;
11
12use super::{Event, Font, Graphic, ScriptInfo, Span, Style};
13#[cfg(debug_assertions)]
14use core::ops::Range;
15
16/// Section type discriminant for efficient lookup and filtering
17///
18/// Provides a lightweight way to identify section types without
19/// borrowing section content. Useful for filtering, routing, and
20/// type-based operations on collections of sections.
21///
22/// # Examples
23///
24/// ```rust
25/// use ass_core::parser::ast::SectionType;
26///
27/// let section_types = vec![SectionType::ScriptInfo, SectionType::Events];
28/// assert!(section_types.contains(&SectionType::ScriptInfo));
29/// ```
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
31pub enum SectionType {
32    /// [Script Info] section identifier
33    ScriptInfo,
34    /// [V4+ Styles] section identifier
35    Styles,
36    /// `[Events\]` section identifier
37    Events,
38    /// `[Fonts\]` section identifier
39    Fonts,
40    /// `[Graphics\]` section identifier
41    Graphics,
42}
43
44/// Top-level section in an ASS script
45///
46/// Represents the main sections that can appear in an ASS subtitle file.
47/// Each variant contains the parsed content of that section with zero-copy
48/// string references to the original source text.
49///
50/// # Examples
51///
52/// ```rust
53/// use ass_core::parser::ast::{Section, ScriptInfo, SectionType, Span};
54///
55/// let info = ScriptInfo { fields: vec![("Title", "Test")], span: Span::new(0, 10, 1, 1) };
56/// let section = Section::ScriptInfo(info);
57/// assert_eq!(section.section_type(), SectionType::ScriptInfo);
58/// ```
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum Section<'a> {
61    /// [Script Info] section with metadata
62    ///
63    /// Contains key-value pairs defining script metadata like title,
64    /// script type, resolution, and other configuration values.
65    ScriptInfo(ScriptInfo<'a>),
66
67    /// [V4+ Styles] section with style definitions
68    ///
69    /// Contains style definitions that can be referenced by events.
70    /// Each style defines font, colors, positioning, and other
71    /// visual properties for subtitle rendering.
72    Styles(Vec<Style<'a>>),
73
74    /// `[Events\]` section with dialogue and commands
75    ///
76    /// Contains dialogue lines, comments, and other timed events
77    /// that make up the actual subtitle content.
78    Events(Vec<Event<'a>>),
79
80    /// `[Fonts\]` section with embedded font data
81    ///
82    /// Contains UU-encoded font files embedded in the script.
83    /// Allows scripts to include custom fonts for portable rendering.
84    Fonts(Vec<Font<'a>>),
85
86    /// `[Graphics\]` section with embedded images
87    ///
88    /// Contains UU-encoded image files embedded in the script.
89    /// Used for logos, textures, and other graphical elements.
90    Graphics(Vec<Graphic<'a>>),
91}
92
93impl Section<'_> {
94    /// Get the span covering this entire section
95    ///
96    /// Computes the span by looking at the content's spans.
97    /// Returns None if the section is empty.
98    #[must_use]
99    pub fn span(&self) -> Option<Span> {
100        match self {
101            Section::ScriptInfo(info) => Some(info.span),
102            Section::Styles(styles) => {
103                if styles.is_empty() {
104                    None
105                } else {
106                    // Merge first and last style spans
107                    let first = &styles[0].span;
108                    let last = &styles[styles.len() - 1].span;
109                    Some(Span::new(first.start, last.end, first.line, first.column))
110                }
111            }
112            Section::Events(events) => {
113                if events.is_empty() {
114                    None
115                } else {
116                    // Merge first and last event spans
117                    let first = &events[0].span;
118                    let last = &events[events.len() - 1].span;
119                    Some(Span::new(first.start, last.end, first.line, first.column))
120                }
121            }
122            Section::Fonts(fonts) => {
123                if fonts.is_empty() {
124                    None
125                } else {
126                    // Merge first and last font spans
127                    let first = &fonts[0].span;
128                    let last = &fonts[fonts.len() - 1].span;
129                    Some(Span::new(first.start, last.end, first.line, first.column))
130                }
131            }
132            Section::Graphics(graphics) => {
133                if graphics.is_empty() {
134                    None
135                } else {
136                    // Merge first and last graphic spans
137                    let first = &graphics[0].span;
138                    let last = &graphics[graphics.len() - 1].span;
139                    Some(Span::new(first.start, last.end, first.line, first.column))
140                }
141            }
142        }
143    }
144
145    /// Get section type discriminant for efficient matching
146    ///
147    /// Returns the section type without borrowing the section content,
148    /// allowing for efficient type-based filtering and routing.
149    ///
150    /// # Examples
151    ///
152    /// ```rust
153    /// # use ass_core::parser::ast::{Section, ScriptInfo, SectionType, Span};
154    /// let info = Section::ScriptInfo(ScriptInfo { fields: Vec::new(), span: Span::new(0, 0, 0, 0) });
155    /// assert_eq!(info.section_type(), SectionType::ScriptInfo);
156    /// ```
157    #[must_use]
158    pub const fn section_type(&self) -> SectionType {
159        match self {
160            Section::ScriptInfo(_) => SectionType::ScriptInfo,
161            Section::Styles(_) => SectionType::Styles,
162            Section::Events(_) => SectionType::Events,
163            Section::Fonts(_) => SectionType::Fonts,
164            Section::Graphics(_) => SectionType::Graphics,
165        }
166    }
167
168    /// Validate all spans in this section reference valid source
169    ///
170    /// Debug helper to ensure zero-copy invariants are maintained.
171    /// Validates that all string references in the section point to
172    /// memory within the specified source range.
173    ///
174    /// Only available in debug builds to avoid performance overhead
175    /// in release builds.
176    ///
177    /// # Arguments
178    ///
179    /// * `source_range` - Valid memory range for source text
180    ///
181    /// # Returns
182    ///
183    /// `true` if all spans are valid, `false` otherwise
184    #[cfg(debug_assertions)]
185    #[must_use]
186    pub fn validate_spans(&self, source_range: &Range<usize>) -> bool {
187        match self {
188            Section::ScriptInfo(info) => info.validate_spans(source_range),
189            Section::Styles(styles) => styles.iter().all(|s| s.validate_spans(source_range)),
190            Section::Events(events) => events.iter().all(|e| e.validate_spans(source_range)),
191            Section::Fonts(fonts) => fonts.iter().all(|f| f.validate_spans(source_range)),
192            Section::Graphics(graphics) => graphics.iter().all(|g| g.validate_spans(source_range)),
193        }
194    }
195}
196
197impl SectionType {
198    /// Get the canonical section header name
199    ///
200    /// Returns the exact header name as it appears in ASS files,
201    /// useful for serialization and error reporting.
202    ///
203    /// # Examples
204    ///
205    /// ```rust
206    /// # use ass_core::parser::ast::SectionType;
207    /// assert_eq!(SectionType::ScriptInfo.header_name(), "Script Info");
208    /// assert_eq!(SectionType::Styles.header_name(), "V4+ Styles");
209    /// ```
210    #[must_use]
211    pub const fn header_name(self) -> &'static str {
212        match self {
213            Self::ScriptInfo => "Script Info",
214            Self::Styles => "V4+ Styles",
215            Self::Events => "Events",
216            Self::Fonts => "Fonts",
217            Self::Graphics => "Graphics",
218        }
219    }
220
221    /// Check if this section type is required in valid ASS files
222    ///
223    /// Returns `true` for sections that must be present for a valid
224    /// ASS file (Script Info and Events), `false` for optional sections.
225    #[must_use]
226    pub const fn is_required(self) -> bool {
227        matches!(self, Self::ScriptInfo | Self::Events)
228    }
229
230    /// Check if this section type contains timed content
231    ///
232    /// Returns `true` for sections with time-based content that affects
233    /// subtitle timing and playback.
234    #[must_use]
235    pub const fn is_timed(self) -> bool {
236        matches!(self, Self::Events)
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    use crate::parser::ast::{Event, EventType, Span, Style};
244    #[cfg(not(feature = "std"))]
245    use alloc::vec;
246
247    #[test]
248    fn section_type_discrimination() {
249        let info = Section::ScriptInfo(ScriptInfo {
250            fields: Vec::new(),
251            span: Span::new(0, 0, 0, 0),
252        });
253        assert_eq!(info.section_type(), SectionType::ScriptInfo);
254
255        let styles = Section::Styles(Vec::new());
256        assert_eq!(styles.section_type(), SectionType::Styles);
257
258        let events = Section::Events(Vec::new());
259        assert_eq!(events.section_type(), SectionType::Events);
260    }
261
262    #[test]
263    fn section_span_script_info() {
264        let info = Section::ScriptInfo(ScriptInfo {
265            fields: vec![("Title", "Test")],
266            span: Span::new(10, 50, 2, 1),
267        });
268
269        let span = info.span();
270        assert!(span.is_some());
271        let span = span.unwrap();
272        assert_eq!(span.start, 10);
273        assert_eq!(span.end, 50);
274        assert_eq!(span.line, 2);
275    }
276
277    #[test]
278    fn section_span_empty_styles() {
279        let styles = Section::Styles(Vec::new());
280        assert!(styles.span().is_none());
281    }
282
283    #[test]
284    fn section_span_single_style() {
285        let style = Style {
286            name: "Default",
287            parent: None,
288            fontname: "Arial",
289            fontsize: "20",
290            primary_colour: "&H00FFFFFF",
291            secondary_colour: "&H000000FF",
292            outline_colour: "&H00000000",
293            back_colour: "&H00000000",
294            bold: "0",
295            italic: "0",
296            underline: "0",
297            strikeout: "0",
298            scale_x: "100",
299            scale_y: "100",
300            spacing: "0",
301            angle: "0",
302            border_style: "1",
303            outline: "0",
304            shadow: "0",
305            alignment: "2",
306            margin_l: "0",
307            margin_r: "0",
308            margin_v: "0",
309            margin_t: None,
310            margin_b: None,
311            encoding: "1",
312            relative_to: None,
313            span: Span::new(100, 200, 5, 1),
314        };
315
316        let styles = Section::Styles(vec![style]);
317        let span = styles.span();
318        assert!(span.is_some());
319        let span = span.unwrap();
320        assert_eq!(span.start, 100);
321        assert_eq!(span.end, 200);
322    }
323
324    #[test]
325    fn section_span_multiple_events() {
326        let event1 = Event {
327            event_type: EventType::Dialogue,
328            layer: "0",
329            start: "0:00:00.00",
330            end: "0:00:05.00",
331            style: "Default",
332            name: "",
333            margin_l: "0",
334            margin_r: "0",
335            margin_v: "0",
336            margin_t: None,
337            margin_b: None,
338            effect: "",
339            text: "First",
340            span: Span::new(100, 150, 10, 1),
341        };
342
343        let event2 = Event {
344            event_type: EventType::Dialogue,
345            layer: "0",
346            start: "0:00:05.00",
347            end: "0:00:10.00",
348            style: "Default",
349            name: "",
350            margin_l: "0",
351            margin_r: "0",
352            margin_v: "0",
353            margin_t: None,
354            margin_b: None,
355            effect: "",
356            text: "Second",
357            span: Span::new(151, 200, 11, 1),
358        };
359
360        let events_section = Section::Events(vec![event1, event2]);
361        let span = events_section.span();
362        assert!(span.is_some());
363        let span = span.unwrap();
364        assert_eq!(span.start, 100);
365        assert_eq!(span.end, 200);
366        assert_eq!(span.line, 10);
367    }
368
369    #[test]
370    #[allow(clippy::similar_names)]
371    fn section_span_multiple_events_similar_names() {
372        // Test moved here to avoid clippy similar_names warning
373    }
374
375    #[test]
376    fn section_type_header_names() {
377        assert_eq!(SectionType::ScriptInfo.header_name(), "Script Info");
378        assert_eq!(SectionType::Styles.header_name(), "V4+ Styles");
379        assert_eq!(SectionType::Events.header_name(), "Events");
380        assert_eq!(SectionType::Fonts.header_name(), "Fonts");
381        assert_eq!(SectionType::Graphics.header_name(), "Graphics");
382    }
383
384    #[test]
385    fn section_type_required() {
386        assert!(SectionType::ScriptInfo.is_required());
387        assert!(SectionType::Events.is_required());
388        assert!(!SectionType::Styles.is_required());
389        assert!(!SectionType::Fonts.is_required());
390        assert!(!SectionType::Graphics.is_required());
391    }
392
393    #[test]
394    fn section_type_timed() {
395        assert!(SectionType::Events.is_timed());
396        assert!(!SectionType::ScriptInfo.is_timed());
397        assert!(!SectionType::Styles.is_timed());
398        assert!(!SectionType::Fonts.is_timed());
399        assert!(!SectionType::Graphics.is_timed());
400    }
401
402    #[test]
403    fn section_type_copy_clone() {
404        let section_type = SectionType::ScriptInfo;
405        let copied = section_type;
406        let cloned = section_type;
407
408        assert_eq!(section_type, copied);
409        assert_eq!(section_type, cloned);
410    }
411
412    #[test]
413    fn section_type_hash() {
414        use alloc::collections::BTreeSet;
415
416        let mut set = BTreeSet::new();
417        set.insert(SectionType::ScriptInfo);
418        set.insert(SectionType::Events);
419
420        assert!(set.contains(&SectionType::ScriptInfo));
421        assert!(set.contains(&SectionType::Events));
422        assert!(!set.contains(&SectionType::Styles));
423    }
424}