ass_core/analysis/
mod.rs

1//! Script analysis and linting for ASS subtitle scripts
2//!
3//! Provides comprehensive analysis capabilities including style resolution,
4//! linting for common issues, and performance optimization suggestions.
5//! Designed for editor integration and script validation.
6//!
7//! # Features
8//!
9//! - Style resolution: Compute effective styles from base + overrides
10//! - Linting rules: Detect common problems and spec violations
11//! - Performance analysis: Identify rendering bottlenecks
12//! - Unicode handling: Bidirectional text and linebreak analysis
13//! - Timing validation: Overlap detection and duration checks
14//!
15//! # Performance
16//!
17//! - Target: <2ms analysis for typical scripts
18//! - Memory: Lazy evaluation to avoid allocation spikes
19//! - Thread-safe: Immutable analysis results
20//!
21//! # Example
22//!
23//! ```rust
24//! use ass_core::{Script, analysis::ScriptAnalysis};
25//!
26//! let script_text = r#"
27//! [Script Info]
28//! Title: Test
29//!
30//! [V4+ Styles]
31//! Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
32//! Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
33//!
34//! [Events\]
35//! Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
36//! Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello World!
37//! "#;
38//!
39//! let script = Script::parse(script_text)?;
40//! let analysis = ScriptAnalysis::analyze(&script)?;
41//!
42//! // Check for issues
43//! for issue in analysis.lint_issues() {
44//!     println!("Warning: {}", issue.message());
45//! }
46//!
47//! // Get resolved styles
48//! if let Some(style) = analysis.resolve_style("Default") {
49//!     println!("Font: {}", style.font_name());
50//! }
51//! # Ok::<(), Box<dyn std::error::Error>>(())
52//! ```
53
54use crate::{
55    parser::{Script, Section},
56    Result,
57};
58
59#[cfg(feature = "plugins")]
60use crate::plugin::ExtensionRegistry;
61use alloc::vec::Vec;
62
63bitflags::bitflags! {
64    /// Script analysis options
65    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
66    pub struct ScriptAnalysisOptions: u8 {
67        /// Enable Unicode linebreak analysis (libass 0.17.4+)
68        const UNICODE_LINEBREAKS = 1 << 0;
69        /// Enable performance warnings
70        const PERFORMANCE_HINTS = 1 << 1;
71        /// Enable strict spec compliance checking
72        const STRICT_COMPLIANCE = 1 << 2;
73        /// Enable bidirectional text analysis
74        const BIDI_ANALYSIS = 1 << 3;
75    }
76}
77
78pub mod events;
79pub mod linting;
80pub mod styles;
81
82pub use events::{
83    count_overlapping_dialogue_events, count_overlapping_events, find_overlapping_dialogue_events,
84    find_overlapping_events, DialogueInfo,
85};
86pub use linting::{lint_script, LintConfig, LintIssue, LintRule};
87pub use styles::{ResolvedStyle, StyleAnalyzer};
88
89/// Comprehensive analysis of an ASS script
90///
91/// Provides linting, style resolution, and performance analysis.
92/// Results are cached for efficient repeated access.
93#[derive(Debug, Clone)]
94pub struct ScriptAnalysis<'a> {
95    /// Reference to analyzed script
96    pub script: &'a Script<'a>,
97
98    /// Detected lint issues
99    lint_issues: Vec<LintIssue>,
100
101    /// Resolved styles cache
102    resolved_styles: Vec<ResolvedStyle<'a>>,
103
104    /// Dialogue analysis results
105    dialogue_info: Vec<DialogueInfo<'a>>,
106
107    /// Analysis configuration
108    config: AnalysisConfig,
109
110    /// Extension registry for custom tag handlers
111    #[cfg(feature = "plugins")]
112    registry: Option<&'a ExtensionRegistry>,
113}
114
115/// Configuration for script analysis
116#[derive(Debug, Clone)]
117pub struct AnalysisConfig {
118    /// Analysis options flags
119    pub options: ScriptAnalysisOptions,
120
121    /// Maximum allowed events for performance warnings
122    pub max_events_threshold: usize,
123}
124
125impl Default for AnalysisConfig {
126    fn default() -> Self {
127        Self {
128            options: ScriptAnalysisOptions::UNICODE_LINEBREAKS
129                | ScriptAnalysisOptions::PERFORMANCE_HINTS
130                | ScriptAnalysisOptions::BIDI_ANALYSIS,
131            max_events_threshold: 1000,
132        }
133    }
134}
135
136impl<'a> ScriptAnalysis<'a> {
137    /// Analyze script with default configuration
138    ///
139    /// Performs comprehensive analysis including linting, style resolution,
140    /// and event analysis. Results are cached for efficient access.
141    /// Analyze ASS script for issues, styles, and content
142    ///
143    /// # Performance
144    ///
145    /// Target <2ms for typical scripts. Uses lazy evaluation for expensive
146    /// operations like Unicode analysis.
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if script analysis fails or contains invalid data.
151    pub fn analyze(script: &'a Script<'a>) -> Result<Self> {
152        #[cfg(feature = "plugins")]
153        return Self::analyze_with_registry(script, None, AnalysisConfig::default());
154        #[cfg(not(feature = "plugins"))]
155        return Self::analyze_with_config(script, AnalysisConfig::default());
156    }
157
158    /// Analyze script with extension registry support
159    ///
160    /// Same as [`analyze`](Self::analyze) but allows custom tag handlers via registry.
161    /// Uses default analysis configuration.
162    ///
163    /// # Arguments
164    ///
165    /// * `script` - Script to analyze
166    /// * `registry` - Optional registry for custom tag handlers
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if script analysis fails or contains invalid data.
171    #[cfg(feature = "plugins")]
172    pub fn analyze_with_registry(
173        script: &'a Script<'a>,
174        registry: Option<&'a ExtensionRegistry>,
175        config: AnalysisConfig,
176    ) -> Result<Self> {
177        Ok(Self::analyze_impl(script, registry, config))
178    }
179
180    /// Analyze script with custom configuration
181    ///
182    /// Allows fine-tuning analysis behavior for specific use cases.
183    ///
184    /// # Errors
185    ///
186    /// Returns an error if script analysis fails or contains invalid data.
187    pub fn analyze_with_config(script: &'a Script<'a>, config: AnalysisConfig) -> Result<Self> {
188        #[cfg(feature = "plugins")]
189        return Ok(Self::analyze_impl(script, None, config));
190        #[cfg(not(feature = "plugins"))]
191        return Ok(Self::analyze_impl_no_plugins(script, config));
192    }
193
194    /// Internal implementation with plugins support
195    #[cfg(feature = "plugins")]
196    fn analyze_impl(
197        script: &'a Script<'a>,
198        registry: Option<&'a ExtensionRegistry>,
199        config: AnalysisConfig,
200    ) -> Self {
201        let mut analysis = Self {
202            script,
203            lint_issues: Vec::new(),
204            resolved_styles: Vec::new(),
205            dialogue_info: Vec::new(),
206            config,
207            registry,
208        };
209
210        analysis.resolve_all_styles();
211        analysis.analyze_events();
212        analysis.run_linting();
213
214        analysis
215    }
216
217    /// Internal implementation without plugins support
218    #[cfg(not(feature = "plugins"))]
219    fn analyze_impl_no_plugins(script: &'a Script<'a>, config: AnalysisConfig) -> Self {
220        let mut analysis = Self {
221            script,
222            lint_issues: Vec::new(),
223            resolved_styles: Vec::new(),
224            dialogue_info: Vec::new(),
225            config,
226        };
227
228        analysis.resolve_all_styles();
229        analysis.analyze_events();
230        analysis.run_linting();
231
232        analysis
233    }
234
235    /// Get all lint issues found during analysis
236    #[must_use]
237    pub fn lint_issues(&self) -> &[LintIssue] {
238        &self.lint_issues
239    }
240
241    /// Get resolved styles
242    #[must_use]
243    pub fn resolved_styles(&self) -> &[ResolvedStyle<'a>] {
244        &self.resolved_styles
245    }
246
247    /// Get dialogue analysis results
248    #[must_use]
249    pub fn dialogue_info(&self) -> &[DialogueInfo<'a>] {
250        &self.dialogue_info
251    }
252
253    /// Get reference to the analyzed script
254    #[must_use]
255    pub const fn script(&self) -> &'a Script<'a> {
256        self.script
257    }
258
259    /// Find resolved style by name
260    #[must_use]
261    pub fn resolve_style(&self, name: &str) -> Option<&ResolvedStyle<'a>> {
262        self.resolved_styles.iter().find(|style| style.name == name)
263    }
264
265    /// Check if script has any critical issues
266    #[must_use]
267    pub fn has_critical_issues(&self) -> bool {
268        self.lint_issues
269            .iter()
270            .any(|issue| issue.severity() == linting::IssueSeverity::Critical)
271    }
272
273    /// Get performance summary
274    #[must_use]
275    pub fn performance_summary(&self) -> PerformanceSummary {
276        PerformanceSummary {
277            total_events: self.dialogue_info.len(),
278            overlapping_events: self.count_overlapping_events(),
279            complex_animations: self.count_complex_animations(),
280            large_fonts: self.count_large_fonts(),
281            performance_score: self.calculate_performance_score(),
282        }
283    }
284
285    /// Run linting analysis
286    fn run_linting(&mut self) {
287        let lint_config = LintConfig::default().with_strict_compliance(
288            self.config
289                .options
290                .contains(ScriptAnalysisOptions::STRICT_COMPLIANCE),
291        );
292
293        let mut issues = Vec::new();
294        let rules = linting::rules::BuiltinRules::all_rules();
295
296        for rule in rules {
297            if !lint_config.is_rule_enabled(rule.id()) {
298                continue;
299            }
300
301            let mut rule_issues = rule.check_script(self);
302            rule_issues.retain(|issue| lint_config.should_report_severity(issue.severity()));
303
304            issues.extend(rule_issues);
305
306            if lint_config.max_issues > 0 && issues.len() >= lint_config.max_issues {
307                issues.truncate(lint_config.max_issues);
308                break;
309            }
310        }
311
312        self.lint_issues = issues;
313    }
314
315    /// Resolve all styles with inheritance and overrides
316    fn resolve_all_styles(&mut self) {
317        let analyzer = StyleAnalyzer::new(self.script);
318        self.resolved_styles = analyzer.resolved_styles().values().cloned().collect();
319    }
320
321    /// Analyze events for timing, overlaps, and performance
322    fn analyze_events(&mut self) {
323        if let Some(Section::Events(events)) = self
324            .script
325            .sections()
326            .iter()
327            .find(|s| matches!(s, Section::Events(_)))
328        {
329            for event in events {
330                #[cfg(feature = "plugins")]
331                let info_result = self.registry.map_or_else(
332                    || DialogueInfo::analyze(event),
333                    |registry| DialogueInfo::analyze_with_registry(event, Some(registry)),
334                );
335
336                #[cfg(not(feature = "plugins"))]
337                let info_result = DialogueInfo::analyze(event);
338
339                if let Ok(info) = info_result {
340                    self.dialogue_info.push(info);
341                }
342            }
343        }
344    }
345
346    /// Count overlapping events using efficient O(n log n) algorithm
347    fn count_overlapping_events(&self) -> usize {
348        count_overlapping_dialogue_events(&self.dialogue_info)
349    }
350
351    /// Count complex animations (transforms, etc.)
352    fn count_complex_animations(&self) -> usize {
353        self.dialogue_info
354            .iter()
355            .filter(|info| info.animation_score() > 3)
356            .count()
357    }
358
359    /// Count fonts larger than reasonable size
360    fn count_large_fonts(&self) -> usize {
361        self.resolved_styles
362            .iter()
363            .filter(|style| style.font_size() > 72.0)
364            .count()
365    }
366
367    /// Calculate overall performance score (0-100)
368    fn calculate_performance_score(&self) -> u8 {
369        let mut score = 100u8;
370
371        if self.dialogue_info.len() > 1000 {
372            score = score.saturating_sub(20);
373        } else if self.dialogue_info.len() > 500 {
374            score = score.saturating_sub(10);
375        }
376
377        let overlaps = self.count_overlapping_events();
378        if overlaps > 50 {
379            score = score.saturating_sub(15);
380        } else if overlaps > 20 {
381            score = score.saturating_sub(8);
382        }
383
384        let animations = self.count_complex_animations();
385        if animations > 100 {
386            score = score.saturating_sub(10);
387        } else if animations > 50 {
388            score = score.saturating_sub(5);
389        }
390
391        let large_fonts = self.count_large_fonts();
392        if large_fonts > 10 {
393            score = score.saturating_sub(5);
394        }
395
396        score
397    }
398}
399
400/// Performance analysis summary
401#[derive(Debug, Clone)]
402pub struct PerformanceSummary {
403    /// Total number of dialogue events
404    pub total_events: usize,
405
406    /// Number of overlapping events
407    pub overlapping_events: usize,
408
409    /// Number of complex animations
410    pub complex_animations: usize,
411
412    /// Number of oversized fonts
413    pub large_fonts: usize,
414
415    /// Overall performance score (0-100, higher is better)
416    pub performance_score: u8,
417}
418
419impl PerformanceSummary {
420    /// Check if script has performance concerns
421    #[must_use]
422    pub const fn has_performance_issues(&self) -> bool {
423        self.performance_score < 80
424    }
425
426    /// Get performance recommendation
427    #[must_use]
428    pub const fn recommendation(&self) -> Option<&'static str> {
429        if self.overlapping_events > 10 {
430            Some("Consider reducing overlapping events for better performance")
431        } else if self.complex_animations > 20 {
432            Some("Many complex animations may impact rendering performance")
433        } else if self.large_fonts > 5 {
434            Some("Large font sizes may cause memory issues")
435        } else if self.total_events > 1000 {
436            Some("Very large script - consider splitting into multiple files")
437        } else {
438            None
439        }
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    #[test]
448    fn analysis_config_default() {
449        let config = AnalysisConfig::default();
450        assert!(config
451            .options
452            .contains(ScriptAnalysisOptions::UNICODE_LINEBREAKS));
453        assert!(config
454            .options
455            .contains(ScriptAnalysisOptions::PERFORMANCE_HINTS));
456        assert!(!config
457            .options
458            .contains(ScriptAnalysisOptions::STRICT_COMPLIANCE));
459        assert_eq!(config.max_events_threshold, 1000);
460    }
461
462    #[test]
463    fn script_analysis_basic() {
464        let script_text = r"
465[Script Info]
466Title: Test Script
467
468[V4+ Styles]
469Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
470Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
471
472[Events\]
473Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
474Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello World!
475Dialogue: 0,0:00:05.00,0:00:10.00,Default,,0,0,0,,Second line
476";
477
478        let script = crate::parser::Script::parse(script_text).unwrap();
479        let analysis = ScriptAnalysis::analyze(&script).unwrap();
480
481        assert_eq!(analysis.lint_issues().len(), 0);
482        assert!(!analysis.has_critical_issues());
483
484        let perf = analysis.performance_summary();
485        assert!(perf.performance_score > 0);
486    }
487
488    #[test]
489    fn performance_summary_recommendations() {
490        let summary = PerformanceSummary {
491            total_events: 100,
492            overlapping_events: 15,
493            complex_animations: 5,
494            large_fonts: 2,
495            performance_score: 75,
496        };
497
498        assert!(summary.has_performance_issues());
499        assert!(summary.recommendation().is_some());
500        assert!(summary
501            .recommendation()
502            .unwrap()
503            .contains("overlapping events"));
504    }
505}