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}