ass_core/parser/
mod.rs

1//! ASS script parser module
2//!
3//! Provides zero-copy parsing of ASS subtitle scripts with lifetime-generic AST nodes.
4//! Supports full ASS v4+, SSA v4 compatibility, and libass 0.17.4+ extensions.
5//!
6//! # Performance
7//!
8//! - Target: <5ms parsing for typical 1KB scripts
9//! - Memory: ~1.1x input size via zero-copy spans
10//! - Incremental updates: <2ms for single-event changes
11//!
12//! # Example
13//!
14//! ```rust
15//! use ass_core::parser::Script;
16//! use ass_core::CoreError;
17//!
18//! let script_text = r#"
19//! [Script Info]
20//! Title: Example
21//! ScriptType: v4.00+
22//!
23//! [Events]
24//! Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
25//! Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello World!
26//! "#;
27//!
28//! let script = Script::parse(script_text)?;
29//! assert_eq!(script.sections().len(), 2);
30//! # Ok::<(), CoreError>(())
31//! ```
32
33pub mod ast;
34pub mod binary_data;
35pub mod errors;
36pub mod incremental;
37pub mod main;
38pub mod position_tracker;
39pub mod script;
40pub mod sections;
41
42#[cfg(feature = "stream")]
43pub mod streaming;
44
45// Re-export public API
46pub use ast::{Event, ScriptInfo, Section, SectionType, Style};
47pub use errors::{IssueCategory, IssueSeverity, ParseError, ParseIssue, ParseResult};
48pub use script::Script;
49#[cfg(feature = "stream")]
50pub use script::{calculate_delta, ScriptDelta, ScriptDeltaOwned};
51
52#[cfg(feature = "stream")]
53pub use streaming::build_modified_source;
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn parse_unknown_section() {
61        let script =
62            Script::parse("[Script Info]\nTitle: Test\n[Unknown Section]\nSomething: here")
63                .unwrap();
64        assert_eq!(script.sections().len(), 1);
65        assert_eq!(script.issues().len(), 1);
66        assert_eq!(script.issues()[0].severity, IssueSeverity::Warning);
67    }
68
69    #[test]
70    fn parse_with_custom_format() {
71        let script_text = r"[Script Info]
72Title: Format Test
73ScriptType: v4.00+
74
75[V4+ Styles]
76Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
77Style: Custom,Arial,20,&H00FF0000&,&H000000FF&,&H00000000&,&H00000000&,1,0,0,0,100,100,0,0,1,2,0,2,15,15,15,1
78
79[Events]
80Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
81Dialogue: 0,0:00:00.00,0:00:05.00,Custom,,0,0,0,,Custom format test
82";
83
84        let script = Script::parse(script_text).unwrap();
85        assert_eq!(script.sections().len(), 3);
86
87        if let Some(Section::Styles(styles)) = script
88            .sections()
89            .iter()
90            .find(|s| matches!(s, Section::Styles(_)))
91        {
92            assert_eq!(styles.len(), 1);
93            let style = &styles[0];
94            assert_eq!(style.name, "Custom");
95            assert_eq!(style.fontname, "Arial");
96            assert_eq!(style.fontsize, "20");
97        } else {
98            panic!("Should have found styles section");
99        }
100
101        if let Some(Section::Events(events)) = script
102            .sections()
103            .iter()
104            .find(|s| matches!(s, Section::Events(_)))
105        {
106            assert_eq!(events.len(), 1);
107            let event = &events[0];
108            assert_eq!(event.start, "0:00:00.00");
109            assert_eq!(event.layer, "0");
110            assert_eq!(event.end, "0:00:05.00");
111            assert_eq!(event.style, "Custom");
112            assert_eq!(event.text, "Custom format test");
113        } else {
114            panic!("Should have found events section");
115        }
116    }
117}