#![allow(dead_code)]
use crate::SubtitleStyle;
use std::collections::HashMap;
#[derive(Clone, Debug)]
pub struct StyleResolution {
pub style: SubtitleStyle,
pub used_fallback: bool,
pub effective_name: String,
}
#[derive(Clone, Debug, Default)]
pub struct StyleOverride {
pub font_size: Option<f32>,
pub primary_color: Option<crate::style::Color>,
pub bold: Option<bool>,
pub italic: Option<bool>,
pub alignment: Option<u8>,
pub margin_left: Option<u32>,
pub margin_right: Option<u32>,
pub margin_v: Option<u32>,
}
#[derive(Clone, Debug, Default)]
pub struct AssStyleCache {
styles: HashMap<String, SubtitleStyle>,
fallback_name: String,
lookup_count: u64,
fallback_count: u64,
}
impl AssStyleCache {
#[must_use]
pub fn new() -> Self {
Self {
styles: HashMap::new(),
fallback_name: "Default".to_string(),
lookup_count: 0,
fallback_count: 0,
}
}
#[must_use]
pub fn from_map(styles: HashMap<String, SubtitleStyle>) -> Self {
Self {
styles,
fallback_name: "Default".to_string(),
lookup_count: 0,
fallback_count: 0,
}
}
#[must_use]
pub fn with_fallback(mut self, name: impl Into<String>) -> Self {
self.fallback_name = name.into();
self
}
pub fn register(&mut self, name: impl Into<String>, style: SubtitleStyle) {
self.styles.insert(name.into(), style);
}
pub fn register_all(
&mut self,
iter: impl IntoIterator<Item = (impl Into<String>, SubtitleStyle)>,
) {
for (name, style) in iter {
self.styles.insert(name.into(), style);
}
}
#[must_use]
pub fn resolve(&mut self, name: &str, overrides: Option<&StyleOverride>) -> StyleResolution {
self.lookup_count += 1;
let (base, effective_name, used_fallback) = if let Some(s) = self.styles.get(name) {
(s.clone(), name.to_string(), false)
} else {
self.fallback_count += 1;
let fb_name = self.fallback_name.clone();
let base = self.styles.get(&fb_name).cloned().unwrap_or_default();
(base, fb_name, true)
};
let style = if let Some(ov) = overrides {
apply_overrides(base, ov)
} else {
base
};
StyleResolution {
style,
used_fallback,
effective_name,
}
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&SubtitleStyle> {
self.styles.get(name)
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.styles.contains_key(name)
}
pub fn remove(&mut self, name: &str) -> Option<SubtitleStyle> {
self.styles.remove(name)
}
#[must_use]
pub fn len(&self) -> usize {
self.styles.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.styles.is_empty()
}
#[must_use]
pub fn style_names(&self) -> Vec<&str> {
self.styles.keys().map(String::as_str).collect()
}
#[must_use]
pub fn lookup_count(&self) -> u64 {
self.lookup_count
}
#[must_use]
pub fn fallback_count(&self) -> u64 {
self.fallback_count
}
#[must_use]
pub fn fallback_ratio(&self) -> f64 {
if self.lookup_count == 0 {
return 0.0;
}
self.fallback_count as f64 / self.lookup_count as f64
}
pub fn reset_metrics(&mut self) {
self.lookup_count = 0;
self.fallback_count = 0;
}
pub fn clear(&mut self) {
self.styles.clear();
self.lookup_count = 0;
self.fallback_count = 0;
}
}
fn apply_overrides(mut base: SubtitleStyle, ov: &StyleOverride) -> SubtitleStyle {
if let Some(size) = ov.font_size {
base.font_size = size;
}
if let Some(color) = ov.primary_color {
base.primary_color = color;
}
if let Some(bold) = ov.bold {
base.font_weight = if bold {
crate::style::FontWeight::Bold
} else {
crate::style::FontWeight::Normal
};
}
if let Some(italic) = ov.italic {
base.font_style = if italic {
crate::style::FontStyle::Italic
} else {
crate::style::FontStyle::Normal
};
}
if let Some(ml) = ov.margin_left {
base.margin_left = ml;
}
if let Some(mr) = ov.margin_right {
base.margin_right = mr;
}
if let Some(mv) = ov.margin_v {
base.margin_bottom = mv;
}
base
}
#[derive(Debug, Default)]
pub struct StyleCacheBuilder {
styles: Vec<(String, SubtitleStyle)>,
fallback: Option<String>,
}
impl StyleCacheBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn add(mut self, name: impl Into<String>, style: SubtitleStyle) -> Self {
self.styles.push((name.into(), style));
self
}
#[must_use]
pub fn fallback(mut self, name: impl Into<String>) -> Self {
self.fallback = Some(name.into());
self
}
#[must_use]
pub fn build(self) -> AssStyleCache {
let mut cache = AssStyleCache::new();
if let Some(fb) = self.fallback {
cache.fallback_name = fb;
}
for (name, style) in self.styles {
cache.register(name, style);
}
cache
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Color;
fn default_style() -> SubtitleStyle {
SubtitleStyle::default()
}
fn bold_style() -> SubtitleStyle {
let mut s = SubtitleStyle::default();
s.font_size = 56.0;
s
}
#[test]
fn test_register_and_get() {
let mut cache = AssStyleCache::new();
cache.register("Default", default_style());
assert!(cache.contains("Default"));
assert!(!cache.contains("Missing"));
}
#[test]
fn test_resolve_known_style() {
let mut cache = AssStyleCache::new();
cache.register("Default", default_style());
let r = cache.resolve("Default", None);
assert!(!r.used_fallback);
assert_eq!(r.effective_name, "Default");
}
#[test]
fn test_resolve_unknown_falls_back() {
let mut cache = AssStyleCache::new();
cache.register("Default", default_style());
let r = cache.resolve("NonExistent", None);
assert!(r.used_fallback);
assert_eq!(r.effective_name, "Default");
}
#[test]
fn test_resolve_missing_fallback_uses_default_trait() {
let mut cache = AssStyleCache::new();
let r = cache.resolve("Anything", None);
assert!(r.used_fallback);
}
#[test]
fn test_style_override_font_size() {
let mut cache = AssStyleCache::new();
cache.register("Default", default_style());
let ov = StyleOverride {
font_size: Some(72.0),
..Default::default()
};
let r = cache.resolve("Default", Some(&ov));
assert!((r.style.font_size - 72.0).abs() < 0.01);
}
#[test]
fn test_style_override_color() {
let mut cache = AssStyleCache::new();
cache.register("Default", default_style());
let red = Color::rgb(255, 0, 0);
let ov = StyleOverride {
primary_color: Some(red),
..Default::default()
};
let r = cache.resolve("Default", Some(&ov));
assert_eq!(r.style.primary_color.r, 255);
assert_eq!(r.style.primary_color.g, 0);
}
#[test]
fn test_lookup_count_increments() {
let mut cache = AssStyleCache::new();
cache.register("Default", default_style());
cache.resolve("Default", None);
cache.resolve("Default", None);
assert_eq!(cache.lookup_count(), 2);
}
#[test]
fn test_fallback_count_increments() {
let mut cache = AssStyleCache::new();
cache.register("Default", default_style());
cache.resolve("Missing", None);
assert_eq!(cache.fallback_count(), 1);
}
#[test]
fn test_fallback_ratio_calculation() {
let mut cache = AssStyleCache::new();
cache.register("Default", default_style());
cache.resolve("Default", None); cache.resolve("Default", None); cache.resolve("Missing", None); let ratio = cache.fallback_ratio();
assert!((ratio - 1.0 / 3.0).abs() < 0.01);
}
#[test]
fn test_register_all() {
let mut cache = AssStyleCache::new();
let styles = vec![
("Default".to_string(), default_style()),
("Title".to_string(), bold_style()),
];
cache.register_all(styles);
assert_eq!(cache.len(), 2);
assert!(cache.contains("Title"));
}
#[test]
fn test_reset_metrics() {
let mut cache = AssStyleCache::new();
cache.register("Default", default_style());
cache.resolve("Default", None);
cache.reset_metrics();
assert_eq!(cache.lookup_count(), 0);
assert_eq!(cache.fallback_count(), 0);
}
#[test]
fn test_clear() {
let mut cache = AssStyleCache::new();
cache.register("Default", default_style());
cache.clear();
assert!(cache.is_empty());
}
#[test]
fn test_builder_pattern() {
let cache = StyleCacheBuilder::new()
.add("Default", default_style())
.add("Bold", bold_style())
.fallback("Default")
.build();
assert_eq!(cache.len(), 2);
assert!(cache.contains("Bold"));
}
#[test]
fn test_remove_style() {
let mut cache = AssStyleCache::new();
cache.register("Default", default_style());
let removed = cache.remove("Default");
assert!(removed.is_some());
assert!(!cache.contains("Default"));
}
#[test]
fn test_style_names_list() {
let mut cache = AssStyleCache::new();
cache.register("Alpha", default_style());
cache.register("Beta", bold_style());
let mut names = cache.style_names();
names.sort();
assert_eq!(names, vec!["Alpha", "Beta"]);
}
#[test]
fn test_bold_override() {
let mut cache = AssStyleCache::new();
cache.register("Default", default_style());
let ov = StyleOverride {
bold: Some(true),
..Default::default()
};
let r = cache.resolve("Default", Some(&ov));
assert_eq!(r.style.font_weight, crate::style::FontWeight::Bold);
}
#[test]
fn test_italic_override() {
let mut cache = AssStyleCache::new();
cache.register("Default", default_style());
let ov = StyleOverride {
italic: Some(true),
..Default::default()
};
let r = cache.resolve("Default", Some(&ov));
assert_eq!(r.style.font_style, crate::style::FontStyle::Italic);
}
#[test]
fn test_custom_fallback_name() {
let mut cache = AssStyleCache::new().with_fallback("Fallback");
cache.register("Fallback", bold_style());
let r = cache.resolve("NonExistent", None);
assert!(r.used_fallback);
assert_eq!(r.effective_name, "Fallback");
assert!((r.style.font_size - bold_style().font_size).abs() < 0.01);
}
#[test]
fn test_fallback_ratio_zero_on_no_lookups() {
let cache = AssStyleCache::new();
assert!((cache.fallback_ratio() - 0.0).abs() < 1e-9);
}
}