1use alloc::string::String;
4use core::ops::Range;
5
6use crate::parser::errors::ParseError;
7use crate::parser::SectionType;
8
9#[cfg(not(feature = "std"))]
10extern crate alloc;
11#[derive(Debug, Clone)]
13pub struct TextChange {
14 pub range: Range<usize>,
16 pub new_text: String,
18 pub line_range: Range<u32>,
20}
21
22#[must_use]
27pub fn adjust_range_for_change(original_range: Range<usize>, change: &TextChange) -> Range<usize> {
28 if change.range.end <= original_range.start {
30 let new_len = change.new_text.len();
31 let old_len = change.range.end - change.range.start;
32
33 if new_len >= old_len {
34 let offset = new_len - old_len;
35 return (original_range.start + offset)..(original_range.end + offset);
36 }
37 let offset = old_len - new_len;
38 return original_range.start.saturating_sub(offset)
39 ..original_range.end.saturating_sub(offset);
40 }
41
42 if change.range.start >= original_range.end {
44 return original_range;
45 }
46
47 let new_start = original_range.start.min(change.range.start);
50
51 let new_len = change.new_text.len();
53 let old_len = change.range.end - change.range.start;
54 let new_end = if change.range.end >= original_range.end {
55 change.range.start + new_len
57 } else {
58 if new_len >= old_len {
60 original_range.end + (new_len - old_len)
61 } else {
62 original_range.end.saturating_sub(old_len - new_len)
63 }
64 };
65
66 new_start..new_end
67}
68
69#[must_use]
71pub fn calculate_line_range(source: &str, byte_range: Range<usize>) -> Range<u32> {
72 let mut line = 1u32;
73 let mut byte_pos = 0;
74 let mut start_line = 0u32;
75 let mut end_line = 0u32;
76
77 for ch in source.chars() {
78 if byte_pos >= byte_range.start && start_line == 0 {
79 start_line = line;
80 }
81 if byte_pos >= byte_range.end {
82 end_line = line;
83 break;
84 }
85 if ch == '\n' {
86 line += 1;
87 }
88 byte_pos += ch.len_utf8();
89 }
90
91 if end_line == 0 {
92 end_line = line;
93 }
94
95 start_line..end_line
96}
97
98#[must_use]
100pub fn calculate_line_number(source: &str, byte_pos: usize) -> u32 {
101 let mut line = 1u32;
102 let mut current_pos = 0;
103
104 for ch in source.chars() {
105 if current_pos >= byte_pos {
106 break;
107 }
108 if ch == '\n' {
109 line += 1;
110 }
111 current_pos += ch.len_utf8();
112 }
113
114 line
115}
116
117pub fn find_section_header_start(
123 source: &str,
124 start_hint: usize,
125 section_type: SectionType,
126) -> Result<usize, ParseError> {
127 let header = match section_type {
129 SectionType::ScriptInfo => "[Script Info]",
130 SectionType::Styles => "[V4+ Styles]",
131 SectionType::Events => "[Events]",
132 SectionType::Fonts => "[Fonts]",
133 SectionType::Graphics => "[Graphics]",
134 };
135
136 let search_start = start_hint.saturating_sub(header.len() + 100); let search_text = &source[search_start..start_hint.min(source.len())];
139
140 search_text
141 .rfind(header)
142 .map_or(Err(ParseError::SectionNotFound), |pos| {
143 let header_pos = search_start + pos;
145 let line_start = source[..header_pos].rfind('\n').map_or(0, |p| p + 1);
146 Ok(line_start)
147 })
148}
149
150pub fn find_section_end(
156 source: &str,
157 end_hint: usize,
158 _section_type: SectionType,
159) -> Result<usize, ParseError> {
160 let section_headers = [
162 "[Script Info]",
163 "[V4+ Styles]",
164 "[Events]",
165 "[Fonts]",
166 "[Graphics]",
167 ];
168
169 let search_text = &source[end_hint..];
170
171 let mut min_pos = None;
173 for header in §ion_headers {
174 if let Some(pos) = search_text.find(header) {
175 min_pos = Some(min_pos.map_or(pos, |min: usize| min.min(pos)));
176 }
177 }
178
179 min_pos.map_or(Ok(source.len()), |pos| {
180 let next_section_pos = end_hint + pos;
182 let line_start = source[..next_section_pos].rfind('\n').map_or(0, |p| p + 1);
183 Ok(line_start)
184 })
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 #[cfg(not(feature = "std"))]
191 use alloc::string::String;
192 #[cfg(not(feature = "std"))]
193 use alloc::string::ToString;
194
195 #[test]
196 fn test_adjust_range_before_change() {
197 let original = 100..200;
198 let change = TextChange {
199 range: 50..60,
200 new_text: "hello".to_string(), line_range: 5..6,
202 };
203
204 let adjusted = adjust_range_for_change(original, &change);
205 assert_eq!(adjusted, 95..195); }
207
208 #[test]
209 fn test_adjust_range_after_change() {
210 let original = 100..200;
211 let change = TextChange {
212 range: 250..260,
213 new_text: "hello".to_string(),
214 line_range: 25..26,
215 };
216
217 let adjusted = adjust_range_for_change(original, &change);
218 assert_eq!(adjusted, 100..200); }
220
221 #[test]
222 fn test_adjust_range_overlapping_change() {
223 let original = 100..200;
224 let change = TextChange {
225 range: 150..160,
226 new_text: "hello world".to_string(), line_range: 15..16,
228 };
229
230 let adjusted = adjust_range_for_change(original, &change);
231 assert_eq!(adjusted, 100..201); }
233
234 #[test]
235 fn test_calculate_line_range() {
236 let source = "line 1\nline 2\nline 3\nline 4\n";
237 let range = calculate_line_range(source, 7..20);
238 assert_eq!(range, 2..3);
239 }
240
241 #[test]
242 fn test_calculate_line_number() {
243 let source = "line 1\nline 2\nline 3\n";
244 assert_eq!(calculate_line_number(source, 0), 1);
245 assert_eq!(calculate_line_number(source, 7), 2);
246 assert_eq!(calculate_line_number(source, 14), 3);
247 }
248
249 #[test]
250 fn test_find_section_header_start() {
251 let source =
252 "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name\n\n[Events]\nFormat: Start";
253
254 let result = find_section_header_start(source, 20, SectionType::ScriptInfo);
256 assert!(result.is_ok());
257 assert_eq!(result.unwrap(), 0);
258
259 let result = find_section_header_start(source, 40, SectionType::Styles);
261 assert!(result.is_ok());
262 assert_eq!(result.unwrap(), 27); let result = find_section_header_start(source, 70, SectionType::Events);
266 assert!(result.is_ok());
267 assert_eq!(result.unwrap(), 54); let result = find_section_header_start(source, 10, SectionType::Events);
271 assert!(result.is_err());
272 }
273
274 #[test]
275 fn test_find_section_end() {
276 let source =
277 "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name\n\n[Events]\nFormat: Start";
278
279 let result = find_section_end(source, 14, SectionType::ScriptInfo);
281 assert!(result.is_ok());
282 assert_eq!(result.unwrap(), 27); let result = find_section_end(source, 42, SectionType::Styles);
286 assert!(result.is_ok());
287 assert_eq!(result.unwrap(), 54); let result = find_section_end(source, 65, SectionType::Events);
291 assert!(result.is_ok());
292 assert_eq!(result.unwrap(), source.len());
293 }
294
295 #[test]
296 fn test_adjust_range_complex_scenarios() {
297 let change = TextChange {
299 range: 10..10,
300 new_text: "inserted".to_string(),
301 line_range: 1..1,
302 };
303 assert_eq!(adjust_range_for_change(20..30, &change), 28..38);
304
305 let change = TextChange {
307 range: 10..20,
308 new_text: String::new(),
309 line_range: 1..2,
310 };
311 assert_eq!(adjust_range_for_change(25..35, &change), 15..25);
312
313 let change = TextChange {
315 range: 10..30,
316 new_text: "replacement".to_string(),
317 line_range: 1..3,
318 };
319 assert_eq!(adjust_range_for_change(15..25, &change), 10..21);
320 }
321
322 #[test]
323 fn test_calculate_line_range_edge_cases() {
324 assert_eq!(calculate_line_range("", 0..0), 0..1); let source = "line1\nline2"; assert_eq!(calculate_line_range(source, 11..11), 0..2); let source = "line1\nline2\nline3";
333 assert_eq!(calculate_line_range(source, 0..17), 1..3);
334
335 let source = "line1\n测试\nline3";
337 assert_eq!(calculate_line_range(source, 6..12), 2..2);
338 }
339}