1use 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 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
66 pub struct ScriptAnalysisOptions: u8 {
67 const UNICODE_LINEBREAKS = 1 << 0;
69 const PERFORMANCE_HINTS = 1 << 1;
71 const STRICT_COMPLIANCE = 1 << 2;
73 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#[derive(Debug, Clone)]
94pub struct ScriptAnalysis<'a> {
95 pub script: &'a Script<'a>,
97
98 lint_issues: Vec<LintIssue>,
100
101 resolved_styles: Vec<ResolvedStyle<'a>>,
103
104 dialogue_info: Vec<DialogueInfo<'a>>,
106
107 config: AnalysisConfig,
109
110 #[cfg(feature = "plugins")]
112 registry: Option<&'a ExtensionRegistry>,
113}
114
115#[derive(Debug, Clone)]
117pub struct AnalysisConfig {
118 pub options: ScriptAnalysisOptions,
120
121 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 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 #[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 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 #[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 #[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 #[must_use]
237 pub fn lint_issues(&self) -> &[LintIssue] {
238 &self.lint_issues
239 }
240
241 #[must_use]
243 pub fn resolved_styles(&self) -> &[ResolvedStyle<'a>] {
244 &self.resolved_styles
245 }
246
247 #[must_use]
249 pub fn dialogue_info(&self) -> &[DialogueInfo<'a>] {
250 &self.dialogue_info
251 }
252
253 #[must_use]
255 pub const fn script(&self) -> &'a Script<'a> {
256 self.script
257 }
258
259 #[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 #[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 #[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 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 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 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 fn count_overlapping_events(&self) -> usize {
348 count_overlapping_dialogue_events(&self.dialogue_info)
349 }
350
351 fn count_complex_animations(&self) -> usize {
353 self.dialogue_info
354 .iter()
355 .filter(|info| info.animation_score() > 3)
356 .count()
357 }
358
359 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 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#[derive(Debug, Clone)]
402pub struct PerformanceSummary {
403 pub total_events: usize,
405
406 pub overlapping_events: usize,
408
409 pub complex_animations: usize,
411
412 pub large_fonts: usize,
414
415 pub performance_score: u8,
417}
418
419impl PerformanceSummary {
420 #[must_use]
422 pub const fn has_performance_issues(&self) -> bool {
423 self.performance_score < 80
424 }
425
426 #[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}