ass_core/analysis/linting/rules/accessibility/
rule.rs1use crate::{
7 analysis::{
8 events::text_analysis::TextAnalysis,
9 linting::{IssueCategory, IssueSeverity, LintIssue, LintRule},
10 ScriptAnalysis,
11 },
12 parser::Section,
13 utils::parse_ass_time,
14};
15use alloc::{format, string::ToString, vec::Vec};
16
17pub struct AccessibilityRule;
54
55impl LintRule for AccessibilityRule {
56 fn id(&self) -> &'static str {
57 "accessibility"
58 }
59
60 fn name(&self) -> &'static str {
61 "Accessibility"
62 }
63
64 fn description(&self) -> &'static str {
65 "Detects potential accessibility issues"
66 }
67
68 fn default_severity(&self) -> IssueSeverity {
69 IssueSeverity::Hint
70 }
71
72 fn category(&self) -> IssueCategory {
73 IssueCategory::Accessibility
74 }
75
76 fn check_script(&self, analysis: &ScriptAnalysis) -> Vec<LintIssue> {
77 let mut issues = Vec::new();
78
79 if let Some(Section::Events(events)) = analysis
80 .script()
81 .sections()
82 .iter()
83 .find(|s| matches!(s, Section::Events(_)))
84 {
85 for event in events {
86 self.check_event_duration(&mut issues, event);
87 self.check_reading_speed(&mut issues, event);
88 self.check_text_length(&mut issues, event);
89 }
90 }
91
92 issues
93 }
94}
95
96impl AccessibilityRule {
97 fn check_event_duration(&self, issues: &mut Vec<LintIssue>, event: &crate::parser::Event) {
99 if let (Ok(start), Ok(end)) = (parse_ass_time(event.start), parse_ass_time(event.end)) {
100 if end >= start {
101 let duration_ms = end - start;
102 if duration_ms < 500 {
103 let issue = LintIssue::new(
104 self.default_severity(),
105 IssueCategory::Accessibility,
106 self.id(),
107 format!("Very short event duration: {duration_ms}ms"),
108 )
109 .with_description("Short durations may be difficult to read".to_string())
110 .with_suggested_fix(
111 "Consider extending duration to at least 500ms for readability".to_string(),
112 );
113
114 issues.push(issue);
115 }
116 }
117 }
118 }
119
120 fn check_reading_speed(&self, issues: &mut Vec<LintIssue>, event: &crate::parser::Event) {
122 if let (Ok(start), Ok(end)) = (parse_ass_time(event.start), parse_ass_time(event.end)) {
123 if end >= start {
124 let duration_centiseconds = end - start;
125
126 if let Ok(analysis) = TextAnalysis::analyze(event.text) {
127 let clean_text_length = analysis.char_count();
128
129 if clean_text_length > 0 && duration_centiseconds > 0 {
130 let duration_seconds = f64::from(duration_centiseconds) / 100.0;
132 let safe_length = u32::try_from(clean_text_length)
133 .unwrap_or(10_000)
134 .min(10_000);
135 let chars_per_second = f64::from(safe_length) / duration_seconds;
136
137 if chars_per_second > 20.0 {
138 let issue = LintIssue::new(
139 self.default_severity(),
140 IssueCategory::Accessibility,
141 self.id(),
142 format!(
143 "Fast reading speed: {chars_per_second:.1} characters/second"
144 ),
145 )
146 .with_description(
147 "Fast reading speeds may be difficult for some users".to_string(),
148 )
149 .with_suggested_fix(
150 "Consider extending duration or reducing text length".to_string(),
151 );
152
153 issues.push(issue);
154 }
155 }
156 }
157 }
158 }
159 }
160
161 fn check_text_length(&self, issues: &mut Vec<LintIssue>, event: &crate::parser::Event) {
163 if let Ok(analysis) = TextAnalysis::analyze(event.text) {
164 let clean_text_length = analysis.char_count();
165
166 if clean_text_length > 200 {
167 let issue = LintIssue::new(
168 self.default_severity(),
169 IssueCategory::Accessibility,
170 self.id(),
171 format!("Very long text: {clean_text_length} characters"),
172 )
173 .with_description("Long text blocks may be overwhelming for some users".to_string())
174 .with_suggested_fix("Consider splitting into multiple shorter events".to_string());
175
176 issues.push(issue);
177 }
178 }
179 }
180}