use crate::{
parser::{Script, Section},
Result,
};
#[cfg(feature = "plugins")]
use crate::plugin::ExtensionRegistry;
use alloc::vec::Vec;
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScriptAnalysisOptions: u8 {
const UNICODE_LINEBREAKS = 1 << 0;
const PERFORMANCE_HINTS = 1 << 1;
const STRICT_COMPLIANCE = 1 << 2;
const BIDI_ANALYSIS = 1 << 3;
}
}
pub mod events;
pub mod linting;
pub mod styles;
pub use events::{
count_overlapping_dialogue_events, count_overlapping_events, find_overlapping_dialogue_events,
find_overlapping_events, DialogueInfo,
};
pub use linting::{lint_script, LintConfig, LintIssue, LintRule};
pub use styles::{ResolvedStyle, StyleAnalyzer};
#[derive(Debug, Clone)]
pub struct ScriptAnalysis<'a> {
pub script: &'a Script<'a>,
lint_issues: Vec<LintIssue>,
resolved_styles: Vec<ResolvedStyle<'a>>,
dialogue_info: Vec<DialogueInfo<'a>>,
config: AnalysisConfig,
#[cfg(feature = "plugins")]
registry: Option<&'a ExtensionRegistry>,
}
#[derive(Debug, Clone)]
pub struct AnalysisConfig {
pub options: ScriptAnalysisOptions,
pub max_events_threshold: usize,
}
impl Default for AnalysisConfig {
fn default() -> Self {
Self {
options: ScriptAnalysisOptions::UNICODE_LINEBREAKS
| ScriptAnalysisOptions::PERFORMANCE_HINTS
| ScriptAnalysisOptions::BIDI_ANALYSIS,
max_events_threshold: 1000,
}
}
}
impl<'a> ScriptAnalysis<'a> {
pub fn analyze(script: &'a Script<'a>) -> Result<Self> {
#[cfg(feature = "plugins")]
return Self::analyze_with_registry(script, None, AnalysisConfig::default());
#[cfg(not(feature = "plugins"))]
return Self::analyze_with_config(script, AnalysisConfig::default());
}
#[cfg(feature = "plugins")]
pub fn analyze_with_registry(
script: &'a Script<'a>,
registry: Option<&'a ExtensionRegistry>,
config: AnalysisConfig,
) -> Result<Self> {
Ok(Self::analyze_impl(script, registry, config))
}
pub fn analyze_with_config(script: &'a Script<'a>, config: AnalysisConfig) -> Result<Self> {
#[cfg(feature = "plugins")]
return Ok(Self::analyze_impl(script, None, config));
#[cfg(not(feature = "plugins"))]
return Ok(Self::analyze_impl_no_plugins(script, config));
}
#[cfg(feature = "plugins")]
fn analyze_impl(
script: &'a Script<'a>,
registry: Option<&'a ExtensionRegistry>,
config: AnalysisConfig,
) -> Self {
let mut analysis = Self {
script,
lint_issues: Vec::new(),
resolved_styles: Vec::new(),
dialogue_info: Vec::new(),
config,
registry,
};
analysis.resolve_all_styles();
analysis.analyze_events();
analysis.run_linting();
analysis
}
#[cfg(not(feature = "plugins"))]
fn analyze_impl_no_plugins(script: &'a Script<'a>, config: AnalysisConfig) -> Self {
let mut analysis = Self {
script,
lint_issues: Vec::new(),
resolved_styles: Vec::new(),
dialogue_info: Vec::new(),
config,
};
analysis.resolve_all_styles();
analysis.analyze_events();
analysis.run_linting();
analysis
}
#[must_use]
pub fn lint_issues(&self) -> &[LintIssue] {
&self.lint_issues
}
#[must_use]
pub fn resolved_styles(&self) -> &[ResolvedStyle<'a>] {
&self.resolved_styles
}
#[must_use]
pub fn dialogue_info(&self) -> &[DialogueInfo<'a>] {
&self.dialogue_info
}
#[must_use]
pub const fn script(&self) -> &'a Script<'a> {
self.script
}
#[must_use]
pub fn resolve_style(&self, name: &str) -> Option<&ResolvedStyle<'a>> {
self.resolved_styles.iter().find(|style| style.name == name)
}
#[must_use]
pub fn has_critical_issues(&self) -> bool {
self.lint_issues
.iter()
.any(|issue| issue.severity() == linting::IssueSeverity::Critical)
}
#[must_use]
pub fn performance_summary(&self) -> PerformanceSummary {
PerformanceSummary {
total_events: self.dialogue_info.len(),
overlapping_events: self.count_overlapping_events(),
complex_animations: self.count_complex_animations(),
large_fonts: self.count_large_fonts(),
performance_score: self.calculate_performance_score(),
}
}
fn run_linting(&mut self) {
let lint_config = LintConfig::default().with_strict_compliance(
self.config
.options
.contains(ScriptAnalysisOptions::STRICT_COMPLIANCE),
);
let mut issues = Vec::new();
let rules = linting::rules::BuiltinRules::all_rules();
for rule in rules {
if !lint_config.is_rule_enabled(rule.id()) {
continue;
}
let mut rule_issues = rule.check_script(self);
rule_issues.retain(|issue| lint_config.should_report_severity(issue.severity()));
issues.extend(rule_issues);
if lint_config.max_issues > 0 && issues.len() >= lint_config.max_issues {
issues.truncate(lint_config.max_issues);
break;
}
}
self.lint_issues = issues;
}
fn resolve_all_styles(&mut self) {
let analyzer = StyleAnalyzer::new(self.script);
self.resolved_styles = analyzer.resolved_styles().values().cloned().collect();
}
fn analyze_events(&mut self) {
if let Some(Section::Events(events)) = self
.script
.sections()
.iter()
.find(|s| matches!(s, Section::Events(_)))
{
for event in events {
#[cfg(feature = "plugins")]
let info_result = self.registry.map_or_else(
|| DialogueInfo::analyze(event),
|registry| DialogueInfo::analyze_with_registry(event, Some(registry)),
);
#[cfg(not(feature = "plugins"))]
let info_result = DialogueInfo::analyze(event);
if let Ok(info) = info_result {
self.dialogue_info.push(info);
}
}
}
}
fn count_overlapping_events(&self) -> usize {
count_overlapping_dialogue_events(&self.dialogue_info)
}
fn count_complex_animations(&self) -> usize {
self.dialogue_info
.iter()
.filter(|info| info.animation_score() > 3)
.count()
}
fn count_large_fonts(&self) -> usize {
self.resolved_styles
.iter()
.filter(|style| style.font_size() > 72.0)
.count()
}
fn calculate_performance_score(&self) -> u8 {
let mut score = 100u8;
if self.dialogue_info.len() > 1000 {
score = score.saturating_sub(20);
} else if self.dialogue_info.len() > 500 {
score = score.saturating_sub(10);
}
let overlaps = self.count_overlapping_events();
if overlaps > 50 {
score = score.saturating_sub(15);
} else if overlaps > 20 {
score = score.saturating_sub(8);
}
let animations = self.count_complex_animations();
if animations > 100 {
score = score.saturating_sub(10);
} else if animations > 50 {
score = score.saturating_sub(5);
}
let large_fonts = self.count_large_fonts();
if large_fonts > 10 {
score = score.saturating_sub(5);
}
score
}
}
#[derive(Debug, Clone)]
pub struct PerformanceSummary {
pub total_events: usize,
pub overlapping_events: usize,
pub complex_animations: usize,
pub large_fonts: usize,
pub performance_score: u8,
}
impl PerformanceSummary {
#[must_use]
pub const fn has_performance_issues(&self) -> bool {
self.performance_score < 80
}
#[must_use]
pub const fn recommendation(&self) -> Option<&'static str> {
if self.overlapping_events > 10 {
Some("Consider reducing overlapping events for better performance")
} else if self.complex_animations > 20 {
Some("Many complex animations may impact rendering performance")
} else if self.large_fonts > 5 {
Some("Large font sizes may cause memory issues")
} else if self.total_events > 1000 {
Some("Very large script - consider splitting into multiple files")
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn analysis_config_default() {
let config = AnalysisConfig::default();
assert!(config
.options
.contains(ScriptAnalysisOptions::UNICODE_LINEBREAKS));
assert!(config
.options
.contains(ScriptAnalysisOptions::PERFORMANCE_HINTS));
assert!(!config
.options
.contains(ScriptAnalysisOptions::STRICT_COMPLIANCE));
assert_eq!(config.max_events_threshold, 1000);
}
#[test]
fn script_analysis_basic() {
let script_text = r"
[Script Info]
Title: Test Script
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
[Events\]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Hello World!
Dialogue: 0,0:00:05.00,0:00:10.00,Default,,0,0,0,,Second line
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analysis = ScriptAnalysis::analyze(&script).unwrap();
assert_eq!(analysis.lint_issues().len(), 0);
assert!(!analysis.has_critical_issues());
let perf = analysis.performance_summary();
assert!(perf.performance_score > 0);
}
#[test]
fn performance_summary_recommendations() {
let summary = PerformanceSummary {
total_events: 100,
overlapping_events: 15,
complex_animations: 5,
large_fonts: 2,
performance_score: 75,
};
assert!(summary.has_performance_issues());
assert!(summary.recommendation().is_some());
assert!(summary
.recommendation()
.unwrap()
.contains("overlapping events"));
}
}