use crate::{
analysis::styles::{
resolved_style::ResolvedStyle,
validation::{StyleConflict, StyleInheritance, StyleValidationIssue},
},
parser::{Script, Section, Style},
};
use alloc::{collections::BTreeMap, collections::BTreeSet, vec::Vec};
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AnalysisOptions: u8 {
const INHERITANCE = 1 << 0;
const CONFLICTS = 1 << 1;
const PERFORMANCE = 1 << 2;
const VALIDATION = 1 << 3;
const STRICT_VALIDATION = 1 << 4;
}
}
#[derive(Debug)]
pub struct StyleAnalyzer<'a> {
script: &'a Script<'a>,
resolved_styles: BTreeMap<&'a str, ResolvedStyle<'a>>,
inheritance_info: BTreeMap<&'a str, StyleInheritance<'a>>,
conflicts: Vec<StyleConflict<'a>>,
config: StyleAnalysisConfig,
resolution_scaling: Option<(f32, f32)>,
}
#[derive(Debug, Clone)]
pub struct StyleAnalysisConfig {
pub options: AnalysisOptions,
pub performance_thresholds: PerformanceThresholds,
}
#[derive(Debug, Clone)]
pub struct PerformanceThresholds {
pub large_font_threshold: f32,
pub large_outline_threshold: f32,
pub large_shadow_threshold: f32,
pub scaling_threshold: f32,
}
impl Default for StyleAnalysisConfig {
fn default() -> Self {
Self {
options: AnalysisOptions::INHERITANCE
| AnalysisOptions::CONFLICTS
| AnalysisOptions::PERFORMANCE
| AnalysisOptions::VALIDATION,
performance_thresholds: PerformanceThresholds::default(),
}
}
}
impl Default for PerformanceThresholds {
fn default() -> Self {
Self {
large_font_threshold: 50.0,
large_outline_threshold: 4.0,
large_shadow_threshold: 4.0,
scaling_threshold: 200.0,
}
}
}
impl<'a> StyleAnalyzer<'a> {
#[must_use]
pub fn new(script: &'a Script<'a>) -> Self {
Self::new_with_config(script, StyleAnalysisConfig::default())
}
#[must_use]
pub fn new_with_config(script: &'a Script<'a>, config: StyleAnalysisConfig) -> Self {
let mut analyzer = Self {
script,
resolved_styles: BTreeMap::new(),
inheritance_info: BTreeMap::new(),
conflicts: Vec::new(),
config,
resolution_scaling: None,
};
analyzer.calculate_resolution_scaling();
analyzer.analyze_all_styles();
analyzer
}
#[must_use]
pub fn resolve_style(&self, name: &str) -> Option<&ResolvedStyle<'a>> {
self.resolved_styles.get(name)
}
#[must_use]
pub const fn resolved_styles(&self) -> &BTreeMap<&'a str, ResolvedStyle<'a>> {
&self.resolved_styles
}
#[must_use]
pub fn conflicts(&self) -> &[StyleConflict<'a>] {
&self.conflicts
}
#[must_use]
pub const fn inheritance_info(&self) -> &BTreeMap<&'a str, StyleInheritance<'a>> {
&self.inheritance_info
}
#[must_use]
pub fn validate_styles(&self) -> Vec<StyleValidationIssue> {
let mut issues = Vec::new();
for resolved in self.resolved_styles.values() {
if self.config.options.contains(AnalysisOptions::VALIDATION) {
issues.extend(self.validate_style_properties(resolved));
}
if self.config.options.contains(AnalysisOptions::PERFORMANCE) {
issues.extend(self.analyze_style_performance(resolved));
}
}
issues
}
fn calculate_resolution_scaling(&mut self) {
for section in self.script.sections() {
if let Section::ScriptInfo(script_info) = section {
let layout_res = script_info.layout_resolution();
let play_res = script_info.play_resolution();
if let (Some((layout_x, layout_y)), Some((play_x, play_y))) = (layout_res, play_res)
{
if layout_x != play_x || layout_y != play_y {
#[allow(clippy::cast_precision_loss)]
let scale_x = play_x as f32 / layout_x as f32;
#[allow(clippy::cast_precision_loss)]
let scale_y = play_y as f32 / layout_y as f32;
self.resolution_scaling = Some((scale_x, scale_y));
}
}
break;
}
}
}
fn analyze_all_styles(&mut self) {
for section in self.script.sections() {
if let Section::Styles(styles) = section {
if let Some(ordered_styles) = self.build_dependency_order(styles) {
self.resolve_styles_with_inheritance(&ordered_styles);
} else {
for style in styles {
if let Ok(mut resolved) = ResolvedStyle::from_style(style) {
if let Some((scale_x, scale_y)) = self.resolution_scaling {
resolved.apply_resolution_scaling(scale_x, scale_y);
}
self.resolved_styles.insert(style.name, resolved);
}
}
}
if self.config.options.contains(AnalysisOptions::CONFLICTS) {
self.detect_style_conflicts_from_section(styles);
}
break;
}
}
}
fn build_dependency_order(&mut self, styles: &'a [Style<'a>]) -> Option<Vec<&'a Style<'a>>> {
let style_map: BTreeMap<&str, &Style> = styles.iter().map(|s| (s.name, s)).collect();
let mut dependencies: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
let mut in_degree: BTreeMap<&str, usize> = BTreeMap::new();
for style in styles {
dependencies.insert(style.name, BTreeSet::new());
in_degree.insert(style.name, 0);
}
for style in styles {
if let Some(parent_name) = style.parent {
if style_map.contains_key(parent_name) {
dependencies
.get_mut(style.name)
.unwrap()
.insert(parent_name);
*in_degree.get_mut(parent_name).unwrap() += 1;
if self.config.options.contains(AnalysisOptions::INHERITANCE) {
if let Some(inheritance) = self.inheritance_info.get_mut(style.name) {
inheritance.set_parent(parent_name);
} else {
let mut inheritance = StyleInheritance::new(style.name);
inheritance.set_parent(parent_name);
self.inheritance_info.insert(style.name, inheritance);
}
}
} else {
self.conflicts
.push(StyleConflict::missing_parent(style.name, parent_name));
}
} else if self.config.options.contains(AnalysisOptions::INHERITANCE) {
self.inheritance_info
.insert(style.name, StyleInheritance::new(style.name));
}
}
if Self::has_circular_dependency(&dependencies) {
self.conflicts.push(StyleConflict::circular_inheritance(
dependencies.keys().copied().collect(),
));
return None;
}
let mut result = Vec::new();
let mut queue: Vec<&str> = Vec::new();
for (name, degree) in &in_degree {
if *degree == 0 {
queue.push(name);
}
}
while let Some(current) = queue.pop() {
if let Some(style) = style_map.get(current) {
result.push(*style);
}
for (child, parents) in &dependencies {
if parents.contains(current) {
if let Some(degree) = in_degree.get_mut(child) {
*degree = degree.saturating_sub(1);
if *degree == 0 {
queue.push(child);
}
}
}
}
}
if result.len() == styles.len() {
Some(result)
} else {
None
}
}
fn has_circular_dependency(dependencies: &BTreeMap<&str, BTreeSet<&str>>) -> bool {
let mut visited = BTreeSet::new();
let mut rec_stack = BTreeSet::new();
for node in dependencies.keys() {
if !visited.contains(node)
&& Self::dfs_has_cycle(node, dependencies, &mut visited, &mut rec_stack)
{
return true;
}
}
false
}
fn dfs_has_cycle<'b>(
node: &'b str,
dependencies: &BTreeMap<&'b str, BTreeSet<&'b str>>,
visited: &mut BTreeSet<&'b str>,
rec_stack: &mut BTreeSet<&'b str>,
) -> bool {
visited.insert(node);
rec_stack.insert(node);
if let Some(neighbors) = dependencies.get(node) {
for neighbor in neighbors {
if !visited.contains(neighbor) {
if Self::dfs_has_cycle(neighbor, dependencies, visited, rec_stack) {
return true;
}
} else if rec_stack.contains(neighbor) {
return true;
}
}
}
rec_stack.remove(node);
false
}
fn resolve_styles_with_inheritance(&mut self, ordered_styles: &[&'a Style<'a>]) {
for style in ordered_styles {
let resolved = if let Some(parent_name) = style.parent {
self.resolved_styles.get(parent_name).map_or_else(
|| ResolvedStyle::from_style(style),
|parent_resolved| ResolvedStyle::from_style_with_parent(style, parent_resolved),
)
} else {
ResolvedStyle::from_style(style)
};
if let Ok(mut resolved_style) = resolved {
if let Some((scale_x, scale_y)) = self.resolution_scaling {
resolved_style.apply_resolution_scaling(scale_x, scale_y);
}
self.resolved_styles.insert(style.name, resolved_style);
}
}
}
#[must_use]
pub fn extract_styles(&self) -> Option<&[Style<'a>]> {
for section in self.script.sections() {
if let Section::Styles(styles) = section {
return Some(styles);
}
}
None
}
fn detect_style_conflicts_from_section(&mut self, styles: &[Style<'a>]) {
let mut name_counts: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for style in styles {
name_counts.entry(style.name).or_default().push(style.name);
}
for (_name, instances) in name_counts {
if instances.len() > 1 {
self.conflicts
.push(StyleConflict::duplicate_name(instances));
}
}
}
fn validate_style_properties(&self, style: &ResolvedStyle<'a>) -> Vec<StyleValidationIssue> {
let mut issues = Vec::new();
if style.font_size() <= 0.0 {
issues.push(StyleValidationIssue::error(
"font_size",
"Font size must be positive",
));
}
if self
.config
.options
.contains(AnalysisOptions::STRICT_VALIDATION)
&& style.font_size() > 200.0
{
issues.push(StyleValidationIssue::warning(
"font_size",
"Very large font size may cause performance issues",
));
}
issues
}
fn analyze_style_performance(&self, style: &ResolvedStyle<'a>) -> Vec<StyleValidationIssue> {
let mut issues = Vec::new();
let thresholds = &self.config.performance_thresholds;
if style.font_size() > thresholds.large_font_threshold {
issues.push(StyleValidationIssue::info_with_suggestion(
"font_size",
"Large font size detected",
"Consider reducing font size for better performance",
));
}
if style.has_performance_issues() {
issues.push(StyleValidationIssue::warning(
"complexity",
"Style has high rendering complexity",
));
}
issues
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::styles::validation::ConflictType;
#[cfg(not(feature = "std"))]
use alloc::format;
#[test]
fn analyzer_creation() {
let script_text = r"
[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
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
assert_eq!(analyzer.resolved_styles().len(), 1);
assert!(analyzer.resolve_style("Default").is_some());
}
#[test]
fn config_defaults() {
let config = StyleAnalysisConfig::default();
assert!(config.options.contains(AnalysisOptions::INHERITANCE));
assert!(config.options.contains(AnalysisOptions::CONFLICTS));
assert!(config.options.contains(AnalysisOptions::VALIDATION));
assert!(!config.options.contains(AnalysisOptions::STRICT_VALIDATION));
}
#[test]
fn performance_thresholds() {
let thresholds = PerformanceThresholds::default();
assert!((thresholds.large_font_threshold - 50.0).abs() < f32::EPSILON);
assert!((thresholds.large_outline_threshold - 4.0).abs() < f32::EPSILON);
assert!((thresholds.large_shadow_threshold - 4.0).abs() < f32::EPSILON);
assert!((thresholds.scaling_threshold - 200.0).abs() < f32::EPSILON);
}
#[test]
fn analyzer_with_custom_config() {
let script_text = r"
[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
";
let script = crate::parser::Script::parse(script_text).unwrap();
let config = StyleAnalysisConfig {
options: AnalysisOptions::VALIDATION | AnalysisOptions::STRICT_VALIDATION,
performance_thresholds: PerformanceThresholds {
large_font_threshold: 30.0,
large_outline_threshold: 2.0,
large_shadow_threshold: 2.0,
scaling_threshold: 150.0,
},
};
let analyzer = StyleAnalyzer::new_with_config(&script, config);
assert_eq!(analyzer.resolved_styles().len(), 1);
assert!(analyzer.resolve_style("Default").is_some());
}
#[test]
fn analyzer_multiple_styles() {
let script_text = r"
[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
Style: Title,Arial,32,&H00FFFF00,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,3,0,2,20,20,20,1
Style: Subtitle,Arial,16,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,1,0,0,100,100,0,0,1,1,0,2,5,5,5,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
assert_eq!(analyzer.resolved_styles().len(), 3);
assert!(analyzer.resolve_style("Default").is_some());
assert!(analyzer.resolve_style("Title").is_some());
assert!(analyzer.resolve_style("Subtitle").is_some());
assert!(analyzer.resolve_style("NonExistent").is_none());
}
#[test]
fn analyzer_duplicate_styles() {
let script_text = r"
[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
Style: Default,Times,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
let conflicts = analyzer.conflicts();
assert!(!conflicts.is_empty());
}
#[test]
fn analyzer_no_styles_section() {
let script_text = r"
[Script Info]
Title: Test Script
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
assert_eq!(analyzer.resolved_styles().len(), 0);
assert!(analyzer.conflicts().is_empty());
}
#[test]
fn analyzer_empty_styles_section() {
let script_text = r"
[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
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
assert_eq!(analyzer.resolved_styles().len(), 0);
assert!(analyzer.conflicts().is_empty());
}
#[test]
fn analyzer_extract_styles() {
let script_text = r"
[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
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
let styles = analyzer.extract_styles();
assert!(styles.is_some());
assert_eq!(styles.unwrap().len(), 1);
}
#[test]
fn analyzer_extract_styles_no_section() {
let script_text = r"
[Script Info]
Title: Test Script
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
let styles = analyzer.extract_styles();
assert!(styles.is_none());
}
#[test]
fn analyzer_inheritance_info() {
let script_text = r"
[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
Style: Title,Arial,32,&H00FFFF00,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,3,0,2,20,20,20,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
let inheritance_info = analyzer.inheritance_info();
assert_eq!(inheritance_info.len(), 2);
assert!(inheritance_info.contains_key("Default"));
assert!(inheritance_info.contains_key("Title"));
}
#[test]
fn analyzer_validate_styles() {
let script_text = r"
[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
Style: Large,Arial,60,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,5,0,2,10,10,10,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
let issues = analyzer.validate_styles();
assert!(issues.is_empty() || !issues.is_empty());
}
#[test]
fn analyzer_strict_validation() {
let script_text = r"
[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: Large,Arial,250,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let config = StyleAnalysisConfig {
options: AnalysisOptions::VALIDATION | AnalysisOptions::STRICT_VALIDATION,
performance_thresholds: PerformanceThresholds::default(),
};
let analyzer = StyleAnalyzer::new_with_config(&script, config);
let issues = analyzer.validate_styles();
assert!(!issues.is_empty());
}
#[test]
fn analyzer_performance_analysis() {
let script_text = r"
[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: Heavy,Arial,60,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,8,5,2,10,10,10,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let config = StyleAnalysisConfig {
options: AnalysisOptions::PERFORMANCE,
performance_thresholds: PerformanceThresholds {
large_font_threshold: 30.0,
large_outline_threshold: 2.0,
large_shadow_threshold: 2.0,
scaling_threshold: 150.0,
},
};
let analyzer = StyleAnalyzer::new_with_config(&script, config);
let issues = analyzer.validate_styles();
assert!(!issues.is_empty());
}
#[test]
fn analyzer_options_flags() {
let options = AnalysisOptions::INHERITANCE | AnalysisOptions::CONFLICTS;
assert!(options.contains(AnalysisOptions::INHERITANCE));
assert!(options.contains(AnalysisOptions::CONFLICTS));
assert!(!options.contains(AnalysisOptions::VALIDATION));
assert!(!options.contains(AnalysisOptions::PERFORMANCE));
assert!(!options.contains(AnalysisOptions::STRICT_VALIDATION));
}
#[test]
fn analyzer_options_debug() {
let options = AnalysisOptions::INHERITANCE;
let debug_str = format!("{options:?}");
assert!(debug_str.contains("INHERITANCE"));
}
#[test]
fn analyzer_config_debug() {
let config = StyleAnalysisConfig::default();
let debug_str = format!("{config:?}");
assert!(debug_str.contains("StyleAnalysisConfig"));
assert!(debug_str.contains("options"));
assert!(debug_str.contains("performance_thresholds"));
}
#[test]
fn analyzer_debug() {
let script_text = r"
[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
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
let debug_str = format!("{analyzer:?}");
assert!(debug_str.contains("StyleAnalyzer"));
}
#[test]
fn performance_thresholds_debug() {
let thresholds = PerformanceThresholds::default();
let debug_str = format!("{thresholds:?}");
assert!(debug_str.contains("PerformanceThresholds"));
assert!(debug_str.contains("large_font_threshold"));
}
#[test]
fn config_clone() {
let config = StyleAnalysisConfig::default();
let cloned = config.clone();
assert_eq!(config.options, cloned.options);
assert!(
(config.performance_thresholds.large_font_threshold
- cloned.performance_thresholds.large_font_threshold)
.abs()
< f32::EPSILON
);
}
#[test]
fn performance_thresholds_clone() {
let thresholds = PerformanceThresholds::default();
let cloned = thresholds.clone();
assert!(
(thresholds.large_font_threshold - cloned.large_font_threshold).abs() < f32::EPSILON
);
assert!(
(thresholds.large_outline_threshold - cloned.large_outline_threshold).abs()
< f32::EPSILON
);
assert!(
(thresholds.large_shadow_threshold - cloned.large_shadow_threshold).abs()
< f32::EPSILON
);
assert!((thresholds.scaling_threshold - cloned.scaling_threshold).abs() < f32::EPSILON);
}
#[test]
fn analyzer_minimal_options() {
let script_text = r"
[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
";
let script = crate::parser::Script::parse(script_text).unwrap();
let config = StyleAnalysisConfig {
options: AnalysisOptions::empty(),
performance_thresholds: PerformanceThresholds::default(),
};
let analyzer = StyleAnalyzer::new_with_config(&script, config);
assert_eq!(analyzer.resolved_styles().len(), 1);
assert!(analyzer.inheritance_info().is_empty());
assert!(analyzer.conflicts().is_empty());
}
#[test]
fn analyzer_style_inheritance_basic() {
let script_text = r"
[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: BaseStyle,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
Style: *BaseStyle,DerivedStyle,Verdana,24,&HFF00FFFF,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
assert_eq!(analyzer.resolved_styles().len(), 2);
let base_style = analyzer.resolve_style("BaseStyle").unwrap();
assert_eq!(base_style.font_name(), "Arial");
assert!((base_style.font_size() - 20.0).abs() < f32::EPSILON);
assert!(!base_style.is_bold());
let derived_style = analyzer.resolve_style("DerivedStyle").unwrap();
assert_eq!(derived_style.font_name(), "Verdana");
assert!((derived_style.font_size() - 24.0).abs() < f32::EPSILON);
assert!(derived_style.is_bold());
assert_eq!(derived_style.primary_color(), [255, 255, 0, 255]); assert_eq!(
derived_style.secondary_color(),
base_style.secondary_color()
);
assert_eq!(derived_style.outline_color(), base_style.outline_color());
}
#[test]
fn analyzer_style_inheritance_partial_override() {
let script_text = r"
[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: BaseStyle,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,1,1,0,0,100,100,0,0,1,2,3,2,10,10,10,1
Style: *BaseStyle,DerivedStyle,Verdana,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,2,3,2,10,10,10,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
let base_style = analyzer.resolve_style("BaseStyle").unwrap();
let derived_style = analyzer.resolve_style("DerivedStyle").unwrap();
assert_eq!(derived_style.font_name(), "Verdana");
assert!((derived_style.font_size() - 24.0).abs() < f32::EPSILON);
assert_eq!(derived_style.primary_color(), base_style.primary_color());
assert!(derived_style.is_bold());
assert!(!derived_style.is_italic());
assert!((derived_style.shadow() - 3.0).abs() < f32::EPSILON);
}
#[test]
fn analyzer_style_inheritance_chain() {
let script_text = r"
[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: GrandParent,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
Style: *GrandParent,Parent,Verdana,24,&H00FFFF00,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,3,0,2,15,15,15,1
Style: *Parent,Child,Times,28,&H00FF00FF,&H000000FF,&H00000000,&H00000000,1,1,0,0,100,100,0,0,1,4,0,2,20,20,20,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
assert_eq!(analyzer.resolved_styles().len(), 3);
let grandparent = analyzer.resolve_style("GrandParent").unwrap();
let parent = analyzer.resolve_style("Parent").unwrap();
let child = analyzer.resolve_style("Child").unwrap();
assert_eq!(grandparent.font_name(), "Arial");
assert!((grandparent.font_size() - 20.0).abs() < f32::EPSILON);
assert!(!grandparent.is_bold());
assert!((grandparent.outline() - 2.0).abs() < f32::EPSILON);
assert_eq!(parent.font_name(), "Verdana");
assert!((parent.font_size() - 24.0).abs() < f32::EPSILON);
assert!(parent.is_bold());
assert!((parent.outline() - 3.0).abs() < f32::EPSILON);
assert_eq!(parent.margin_l(), 15);
assert_eq!(child.font_name(), "Times");
assert!((child.font_size() - 28.0).abs() < f32::EPSILON);
assert!(child.is_bold());
assert!(child.is_italic());
assert!((child.outline() - 4.0).abs() < f32::EPSILON);
assert_eq!(child.margin_l(), 20);
}
#[test]
fn analyzer_style_inheritance_missing_parent() {
let script_text = r"
[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: *NonExistent,Orphan,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
assert_eq!(analyzer.resolved_styles().len(), 1);
let orphan = analyzer.resolve_style("Orphan").unwrap();
assert_eq!(orphan.font_name(), "Arial");
let conflicts = analyzer.conflicts();
assert!(!conflicts.is_empty());
assert!(conflicts
.iter()
.any(|c| matches!(c.conflict_type, ConflictType::MissingReference)));
}
#[test]
fn analyzer_style_circular_inheritance() {
let script_text = r"
[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: *StyleB,StyleA,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
Style: *StyleA,StyleB,Verdana,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
assert_eq!(analyzer.resolved_styles().len(), 2);
let conflicts = analyzer.conflicts();
assert!(conflicts
.iter()
.any(|c| matches!(c.conflict_type, ConflictType::CircularInheritance)));
}
#[test]
fn analyzer_style_self_inheritance() {
let script_text = r"
[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: *SelfRef,SelfRef,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
assert_eq!(analyzer.resolved_styles().len(), 1);
let conflicts = analyzer.conflicts();
assert!(conflicts
.iter()
.any(|c| matches!(c.conflict_type, ConflictType::CircularInheritance)));
}
#[test]
fn analyzer_inheritance_info_tracking() {
let script_text = r"
[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: BaseStyle,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
Style: *BaseStyle,Child1,Verdana,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
Style: *BaseStyle,Child2,Times,18,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
let inheritance_info = analyzer.inheritance_info();
assert_eq!(inheritance_info.len(), 3);
let base_info = inheritance_info.get("BaseStyle").unwrap();
assert!(base_info.is_root());
assert!(base_info.parents.is_empty());
let child1_info = inheritance_info.get("Child1").unwrap();
assert!(!child1_info.is_root());
assert_eq!(child1_info.parents.len(), 1);
assert_eq!(child1_info.parents[0], "BaseStyle");
let child2_info = inheritance_info.get("Child2").unwrap();
assert!(!child2_info.is_root());
assert_eq!(child2_info.parents.len(), 1);
assert_eq!(child2_info.parents[0], "BaseStyle");
}
#[test]
fn analyzer_layout_resolution_scaling() {
let script_text = r"
[Script Info]
Title: Resolution Scaling Test
LayoutResX: 640
LayoutResY: 480
PlayResX: 1280
PlayResY: 960
[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,2,0,1,4,2,2,10,10,20,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
let default_style = analyzer.resolve_style("Default").unwrap();
assert!((default_style.font_size() - 40.0).abs() < f32::EPSILON); assert!((default_style.spacing() - 4.0).abs() < f32::EPSILON); assert!((default_style.outline() - 8.0).abs() < f32::EPSILON); assert!((default_style.shadow() - 4.0).abs() < f32::EPSILON); assert_eq!(default_style.margin_l(), 20); assert_eq!(default_style.margin_r(), 20); assert_eq!(default_style.margin_t(), 40); assert_eq!(default_style.margin_b(), 40); }
#[test]
fn analyzer_layout_resolution_scaling_asymmetric() {
let script_text = r"
[Script Info]
Title: Asymmetric Resolution Scaling Test
LayoutResX: 640
LayoutResY: 480
PlayResX: 1920
PlayResY: 1080
[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,2,0,1,4,2,2,10,10,20,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
let default_style = analyzer.resolve_style("Default").unwrap();
let avg_scale = 2.625;
assert!((20.0f32.mul_add(-avg_scale, default_style.font_size())).abs() < 0.01);
assert!((default_style.spacing() - 6.0).abs() < f32::EPSILON); assert!((4.0f32.mul_add(-avg_scale, default_style.outline())).abs() < 0.01);
assert!((2.0f32.mul_add(-avg_scale, default_style.shadow())).abs() < 0.01);
assert_eq!(default_style.margin_l(), 30); assert_eq!(default_style.margin_r(), 30); assert_eq!(default_style.margin_t(), 45); assert_eq!(default_style.margin_b(), 45); }
#[test]
fn analyzer_no_resolution_scaling_when_same() {
let script_text = r"
[Script Info]
Title: No Scaling Test
LayoutResX: 1920
LayoutResY: 1080
PlayResX: 1920
PlayResY: 1080
[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,2,0,1,4,2,2,10,10,20,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
let default_style = analyzer.resolve_style("Default").unwrap();
assert!((default_style.font_size() - 20.0).abs() < f32::EPSILON);
assert!((default_style.spacing() - 2.0).abs() < f32::EPSILON);
assert!((default_style.outline() - 4.0).abs() < f32::EPSILON);
assert!((default_style.shadow() - 2.0).abs() < f32::EPSILON);
assert_eq!(default_style.margin_l(), 10);
assert_eq!(default_style.margin_r(), 10);
assert_eq!(default_style.margin_t(), 20);
assert_eq!(default_style.margin_b(), 20);
}
#[test]
fn analyzer_resolution_scaling_with_inheritance() {
let script_text = r"
[Script Info]
Title: Scaling with Inheritance Test
LayoutResX: 640
LayoutResY: 480
PlayResX: 1280
PlayResY: 960
[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: Base,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,2,0,1,4,2,2,10,10,20,1
Style: *Base,Derived,Verdana,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,15,15,20,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
let base_style = analyzer.resolve_style("Base").unwrap();
let derived_style = analyzer.resolve_style("Derived").unwrap();
assert!((base_style.font_size() - 40.0).abs() < f32::EPSILON);
assert!((derived_style.font_size() - 48.0).abs() < f32::EPSILON);
assert_eq!(derived_style.margin_l(), 30); assert_eq!(derived_style.margin_r(), 30); assert_eq!(derived_style.margin_t(), 40); assert_eq!(derived_style.margin_b(), 40); }
#[test]
fn analyzer_no_resolution_info_no_scaling() {
let script_text = r"
[Script Info]
Title: No Resolution Info Test
[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,2,0,1,4,2,2,10,10,20,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
let default_style = analyzer.resolve_style("Default").unwrap();
assert!((default_style.font_size() - 20.0).abs() < f32::EPSILON);
assert!((default_style.spacing() - 2.0).abs() < f32::EPSILON);
assert!((default_style.outline() - 4.0).abs() < f32::EPSILON);
assert!((default_style.shadow() - 2.0).abs() < f32::EPSILON);
assert_eq!(default_style.margin_l(), 10);
assert_eq!(default_style.margin_r(), 10);
assert_eq!(default_style.margin_t(), 20);
assert_eq!(default_style.margin_b(), 20);
}
#[test]
fn analyzer_partial_resolution_info_no_scaling() {
let script_text = r"
[Script Info]
Title: Partial Resolution Info Test
LayoutResX: 640
PlayResY: 960
[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,2,0,1,4,2,2,10,10,20,1
";
let script = crate::parser::Script::parse(script_text).unwrap();
let analyzer = StyleAnalyzer::new(&script);
let default_style = analyzer.resolve_style("Default").unwrap();
assert!((default_style.font_size() - 20.0).abs() < f32::EPSILON);
assert!((default_style.spacing() - 2.0).abs() < f32::EPSILON);
assert!((default_style.outline() - 4.0).abs() < f32::EPSILON);
assert!((default_style.shadow() - 2.0).abs() < f32::EPSILON);
assert_eq!(default_style.margin_l(), 10);
assert_eq!(default_style.margin_r(), 10);
assert_eq!(default_style.margin_t(), 20);
assert_eq!(default_style.margin_b(), 20);
}
}