ass_core/
lib.rs

1//! # ASS-RS Core
2//!
3//! High-performance, memory-efficient ASS (Advanced `SubStation` Alpha) subtitle format parser,
4//! analyzer, and manipulator. Surpasses libass in modularity, reusability, and efficiency
5//! through zero-copy parsing, trait-based extensibility, and strict memory management.
6//!
7//! ## Features
8//!
9//! - **Zero-copy parsing**: Uses `&str` spans to avoid allocations
10//! - **Incremental updates**: Partial re-parsing for <2ms edits
11//! - **SIMD optimization**: Feature-gated performance improvements
12//! - **Extensible plugins**: Runtime tag and section handlers
13//! - **Strict compliance**: Full ASS v4+, SSA v4, and libass 0.17.4+ support
14//! - **Thread-safe**: Immutable `Script` design with `Send + Sync`
15//!
16//! ## Quick Start
17//!
18//! ```rust
19//! use ass_core::{Script, analysis::ScriptAnalysis};
20//!
21//! let script_text = r#"
22//! [Script Info]
23//! Title: Example
24//! ScriptType: v4.00+
25//!
26//! [V4+ Styles]
27//! Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
28//! Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
29//!
30//! [Events]
31//! Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
32//! Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello World!
33//! "#;
34//!
35//! let script = Script::parse(script_text)?;
36//! let analysis = ScriptAnalysis::analyze(&script)?;
37//! # Ok::<(), Box<dyn std::error::Error>>(())
38//! ```
39//!
40//! ## Performance Targets
41//!
42//! - Parse: <5ms for 1KB scripts
43//! - Analysis: <2ms for typical content
44//! - Memory: ~1.1x input size via zero-copy spans
45//! - Incremental: <2ms for single-event updates
46
47#![cfg_attr(not(feature = "std"), no_std)]
48#![cfg_attr(docsrs, feature(doc_cfg))]
49#![deny(clippy::all)]
50#![deny(unsafe_code)]
51#![allow(clippy::negative_feature_names)]
52
53// Always make alloc available, whether in std or no_std mode
54extern crate alloc;
55
56pub mod parser;
57pub mod tokenizer;
58
59#[cfg(feature = "analysis")]
60#[cfg_attr(docsrs, doc(cfg(feature = "analysis")))]
61pub mod analysis;
62
63#[cfg(feature = "plugins")]
64#[cfg_attr(docsrs, doc(cfg(feature = "plugins")))]
65pub mod plugin;
66
67pub mod utils;
68
69pub use parser::{ParseError, Script, Section};
70pub use tokenizer::{AssTokenizer, Token};
71
72#[cfg(feature = "analysis")]
73pub use analysis::ScriptAnalysis;
74
75#[cfg(feature = "plugins")]
76pub use plugin::ExtensionRegistry;
77
78pub use utils::{CoreError, Spans};
79
80/// Crate version for runtime compatibility checks
81pub const VERSION: &str = env!("CARGO_PKG_VERSION");
82
83/// Supported ASS script versions for compatibility and feature detection.
84///
85/// ASS scripts can declare different versions that affect parsing behavior
86/// and available features. This enum helps determine which parsing mode
87/// to use and which features are available.
88///
89/// # Examples
90///
91/// ```rust
92/// use ass_core::ScriptVersion;
93///
94/// // Parse from header
95/// let version = ScriptVersion::from_header("v4.00+").unwrap();
96/// assert_eq!(version, ScriptVersion::AssV4);
97///
98/// // Check feature support
99/// assert!(!ScriptVersion::SsaV4.supports_extensions());
100/// assert!(ScriptVersion::AssV4Plus.supports_extensions());
101/// ```
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
103pub enum ScriptVersion {
104    /// SSA v4.00 (`SubStation` Alpha legacy format).
105    ///
106    /// Provides compatibility with legacy SSA files. Limited feature set
107    /// compared to modern ASS versions.
108    SsaV4,
109    /// ASS v4.00+ (Advanced `SubStation` Alpha standard).
110    ///
111    /// The most common format used by modern subtitle tools. Supports
112    /// all standard ASS features and tags.
113    AssV4,
114    /// ASS v4.00+ with extensions (libass 0.17.4+ compatibility).
115    ///
116    /// Extended format supporting newer features like `\kt` karaoke tags,
117    /// Unicode line wrapping, and other libass extensions.
118    AssV4Plus,
119}
120
121impl ScriptVersion {
122    /// Parse script version from a `ScriptType` header value.
123    ///
124    /// Converts header strings commonly found in `[Script Info]` sections
125    /// to the appropriate script version enum. Handles various formats
126    /// including extended versions.
127    ///
128    /// # Arguments
129    ///
130    /// * `header` - The header value string (usually from `ScriptType` field)
131    ///
132    /// # Returns
133    ///
134    /// Returns `Some(ScriptVersion)` if the header is recognized, or `None`
135    /// if the version string is invalid or unsupported.
136    ///
137    /// # Examples
138    ///
139    /// ```rust
140    /// use ass_core::ScriptVersion;
141    ///
142    /// assert_eq!(ScriptVersion::from_header("v4.00"), Some(ScriptVersion::SsaV4));
143    /// assert_eq!(ScriptVersion::from_header("v4.00+"), Some(ScriptVersion::AssV4));
144    /// assert_eq!(ScriptVersion::from_header("v4.00++"), Some(ScriptVersion::AssV4Plus));
145    /// assert_eq!(ScriptVersion::from_header("invalid"), None);
146    /// ```
147    #[must_use]
148    pub fn from_header(header: &str) -> Option<Self> {
149        match header.trim() {
150            "v4.00" => Some(Self::SsaV4),
151            "v4.00+" => Some(Self::AssV4),
152            "v4.00++" | "v4.00+ extended" => Some(Self::AssV4Plus),
153            _ => None,
154        }
155    }
156
157    /// Check if the script version supports modern ASS extensions.
158    ///
159    /// Modern ASS extensions include features like:
160    /// - `\kt` karaoke timing tags
161    /// - Unicode line wrapping
162    /// - Extended color formats
163    /// - Advanced animation features
164    ///
165    /// Only `AssV4Plus` currently supports these extensions, as they
166    /// require libass 0.17.4+ compatibility.
167    ///
168    /// # Returns
169    ///
170    /// Returns `true` if the version supports extensions, `false` otherwise.
171    ///
172    /// # Examples
173    ///
174    /// ```rust
175    /// use ass_core::ScriptVersion;
176    ///
177    /// assert!(!ScriptVersion::SsaV4.supports_extensions());
178    /// assert!(!ScriptVersion::AssV4.supports_extensions());
179    /// assert!(ScriptVersion::AssV4Plus.supports_extensions());
180    /// ```
181    #[must_use]
182    pub const fn supports_extensions(self) -> bool {
183        matches!(self, Self::AssV4Plus)
184    }
185}
186
187/// Result type for core operations, using the crate's unified `CoreError`.
188///
189/// This type alias provides a convenient way to return results from core operations
190/// without having to specify the error type explicitly in every function signature.
191///
192/// # Examples
193///
194/// ```rust
195/// use ass_core::{Result, Script};
196///
197/// fn parse_script_safely(input: &str) -> Result<Script> {
198///     Script::parse(input)
199/// }
200/// ```
201pub type Result<T> = core::result::Result<T, CoreError>;
202
203#[cfg(test)]
204mod integration_tests {
205    use super::*;
206    #[cfg(feature = "analysis")]
207    use crate::analysis::{
208        linting::{lint_script, LintConfig},
209        ScriptAnalysis,
210    };
211
212    /// Comprehensive integration test verifying core functionality works correctly
213    #[test]
214    fn test_core_functionality_integration() {
215        let script_text = r"
216[Script Info]
217Title: Test Script
218ScriptType: v4.00+
219
220[V4+ Styles]
221Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
222Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
223Style: Large,Arial,80,&H00FF0000,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
224
225[Events]
226Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
227Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello World!
228Dialogue: 0,0:00:02.00,0:00:07.00,Default,,0,0,0,,Overlapping dialogue
229Dialogue: 0,0:00:10.00,0:00:15.00,Large,,0,0,0,,{\t(0,1000,\fscx200\fscy200)}Large animated text
230Comment: 0,0:00:30.00,0:00:35.00,Default,,0,0,0,,This is a comment
231";
232
233        let script = Script::parse(script_text).expect("Should parse script successfully");
234        assert!(
235            script.sections().len() >= 2,
236            "Should have parsed multiple sections"
237        );
238
239        let version = ScriptVersion::from_header("v4.00+").expect("Should detect script version");
240        assert_eq!(version, ScriptVersion::AssV4);
241        assert!(!version.supports_extensions());
242
243        let version_plus =
244            ScriptVersion::from_header("v4.00++").expect("Should detect extended version");
245        assert_eq!(version_plus, ScriptVersion::AssV4Plus);
246        assert!(version_plus.supports_extensions());
247
248        #[cfg(feature = "analysis")]
249        {
250            let analysis =
251                ScriptAnalysis::analyze(&script).expect("Should analyze script successfully");
252
253            assert!(
254                !analysis.resolved_styles().is_empty(),
255                "Should resolve styles"
256            );
257
258            assert!(
259                !analysis.dialogue_info().is_empty(),
260                "Should analyze dialogue events"
261            );
262
263            let perf = analysis.performance_summary();
264            assert!(
265                perf.performance_score <= 100,
266                "Performance score should be valid"
267            );
268
269            let default_style = analysis.resolve_style("Default");
270            assert!(default_style.is_some(), "Should find Default style");
271
272            let lint_config = LintConfig::default();
273            let issues =
274                lint_script(&script, &lint_config).expect("Should run linting successfully");
275
276            assert!(!issues.is_empty(), "Should detect some lint issues");
277        }
278    }
279
280    #[test]
281    fn test_script_version_functionality() {
282        assert_eq!(
283            ScriptVersion::from_header("v4.00"),
284            Some(ScriptVersion::SsaV4)
285        );
286        assert_eq!(
287            ScriptVersion::from_header("v4.00+"),
288            Some(ScriptVersion::AssV4)
289        );
290        assert_eq!(
291            ScriptVersion::from_header("v4.00++"),
292            Some(ScriptVersion::AssV4Plus)
293        );
294        assert_eq!(
295            ScriptVersion::from_header("v4.00+ extended"),
296            Some(ScriptVersion::AssV4Plus)
297        );
298        assert_eq!(ScriptVersion::from_header("invalid"), None);
299
300        assert!(!ScriptVersion::SsaV4.supports_extensions());
301        assert!(!ScriptVersion::AssV4.supports_extensions());
302        assert!(ScriptVersion::AssV4Plus.supports_extensions());
303    }
304
305    #[test]
306    fn test_error_handling() {
307        let invalid_script = "This is not a valid ASS script";
308        let result = Script::parse(invalid_script);
309
310        if let Ok(script) = result {
311            assert!(
312                !script.issues().is_empty(),
313                "Invalid script should have parse issues"
314            );
315        }
316    }
317
318    #[test]
319    fn test_empty_script_handling() {
320        let empty_script = "";
321        let result = Script::parse(empty_script);
322
323        assert!(result.is_ok(), "Should handle empty script gracefully");
324
325        let script = result.unwrap();
326        assert_eq!(
327            script.sections().len(),
328            0,
329            "Empty script should have no sections"
330        );
331    }
332}