use alloc::string::String;
use core::ops::Range;
use crate::parser::errors::ParseError;
use crate::parser::SectionType;
#[cfg(not(feature = "std"))]
extern crate alloc;
#[derive(Debug, Clone)]
pub struct TextChange {
pub range: Range<usize>,
pub new_text: String,
pub line_range: Range<u32>,
}
#[must_use]
pub fn adjust_range_for_change(original_range: Range<usize>, change: &TextChange) -> Range<usize> {
if change.range.end <= original_range.start {
let new_len = change.new_text.len();
let old_len = change.range.end - change.range.start;
if new_len >= old_len {
let offset = new_len - old_len;
return (original_range.start + offset)..(original_range.end + offset);
}
let offset = old_len - new_len;
return original_range.start.saturating_sub(offset)
..original_range.end.saturating_sub(offset);
}
if change.range.start >= original_range.end {
return original_range;
}
let new_start = original_range.start.min(change.range.start);
let new_len = change.new_text.len();
let old_len = change.range.end - change.range.start;
let new_end = if change.range.end >= original_range.end {
change.range.start + new_len
} else {
if new_len >= old_len {
original_range.end + (new_len - old_len)
} else {
original_range.end.saturating_sub(old_len - new_len)
}
};
new_start..new_end
}
#[must_use]
pub fn calculate_line_range(source: &str, byte_range: Range<usize>) -> Range<u32> {
let mut line = 1u32;
let mut byte_pos = 0;
let mut start_line = 0u32;
let mut end_line = 0u32;
for ch in source.chars() {
if byte_pos >= byte_range.start && start_line == 0 {
start_line = line;
}
if byte_pos >= byte_range.end {
end_line = line;
break;
}
if ch == '\n' {
line += 1;
}
byte_pos += ch.len_utf8();
}
if end_line == 0 {
end_line = line;
}
start_line..end_line
}
#[must_use]
pub fn calculate_line_number(source: &str, byte_pos: usize) -> u32 {
let mut line = 1u32;
let mut current_pos = 0;
for ch in source.chars() {
if current_pos >= byte_pos {
break;
}
if ch == '\n' {
line += 1;
}
current_pos += ch.len_utf8();
}
line
}
pub fn find_section_header_start(
source: &str,
start_hint: usize,
section_type: SectionType,
) -> Result<usize, ParseError> {
let header = match section_type {
SectionType::ScriptInfo => "[Script Info]",
SectionType::Styles => "[V4+ Styles]",
SectionType::Events => "[Events]",
SectionType::Fonts => "[Fonts]",
SectionType::Graphics => "[Graphics]",
};
let search_start = start_hint.saturating_sub(header.len() + 100); let search_text = &source[search_start..start_hint.min(source.len())];
search_text
.rfind(header)
.map_or(Err(ParseError::SectionNotFound), |pos| {
let header_pos = search_start + pos;
let line_start = source[..header_pos].rfind('\n').map_or(0, |p| p + 1);
Ok(line_start)
})
}
pub fn find_section_end(
source: &str,
end_hint: usize,
_section_type: SectionType,
) -> Result<usize, ParseError> {
let section_headers = [
"[Script Info]",
"[V4+ Styles]",
"[Events]",
"[Fonts]",
"[Graphics]",
];
let search_text = &source[end_hint..];
let mut min_pos = None;
for header in §ion_headers {
if let Some(pos) = search_text.find(header) {
min_pos = Some(min_pos.map_or(pos, |min: usize| min.min(pos)));
}
}
min_pos.map_or(Ok(source.len()), |pos| {
let next_section_pos = end_hint + pos;
let line_start = source[..next_section_pos].rfind('\n').map_or(0, |p| p + 1);
Ok(line_start)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
use alloc::string::String;
#[cfg(not(feature = "std"))]
use alloc::string::ToString;
#[test]
fn test_adjust_range_before_change() {
let original = 100..200;
let change = TextChange {
range: 50..60,
new_text: "hello".to_string(), line_range: 5..6,
};
let adjusted = adjust_range_for_change(original, &change);
assert_eq!(adjusted, 95..195); }
#[test]
fn test_adjust_range_after_change() {
let original = 100..200;
let change = TextChange {
range: 250..260,
new_text: "hello".to_string(),
line_range: 25..26,
};
let adjusted = adjust_range_for_change(original, &change);
assert_eq!(adjusted, 100..200); }
#[test]
fn test_adjust_range_overlapping_change() {
let original = 100..200;
let change = TextChange {
range: 150..160,
new_text: "hello world".to_string(), line_range: 15..16,
};
let adjusted = adjust_range_for_change(original, &change);
assert_eq!(adjusted, 100..201); }
#[test]
fn test_calculate_line_range() {
let source = "line 1\nline 2\nline 3\nline 4\n";
let range = calculate_line_range(source, 7..20);
assert_eq!(range, 2..3);
}
#[test]
fn test_calculate_line_number() {
let source = "line 1\nline 2\nline 3\n";
assert_eq!(calculate_line_number(source, 0), 1);
assert_eq!(calculate_line_number(source, 7), 2);
assert_eq!(calculate_line_number(source, 14), 3);
}
#[test]
fn test_find_section_header_start() {
let source =
"[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name\n\n[Events]\nFormat: Start";
let result = find_section_header_start(source, 20, SectionType::ScriptInfo);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0);
let result = find_section_header_start(source, 40, SectionType::Styles);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 27);
let result = find_section_header_start(source, 70, SectionType::Events);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 54);
let result = find_section_header_start(source, 10, SectionType::Events);
assert!(result.is_err());
}
#[test]
fn test_find_section_end() {
let source =
"[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name\n\n[Events]\nFormat: Start";
let result = find_section_end(source, 14, SectionType::ScriptInfo);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 27);
let result = find_section_end(source, 42, SectionType::Styles);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 54);
let result = find_section_end(source, 65, SectionType::Events);
assert!(result.is_ok());
assert_eq!(result.unwrap(), source.len());
}
#[test]
fn test_adjust_range_complex_scenarios() {
let change = TextChange {
range: 10..10,
new_text: "inserted".to_string(),
line_range: 1..1,
};
assert_eq!(adjust_range_for_change(20..30, &change), 28..38);
let change = TextChange {
range: 10..20,
new_text: String::new(),
line_range: 1..2,
};
assert_eq!(adjust_range_for_change(25..35, &change), 15..25);
let change = TextChange {
range: 10..30,
new_text: "replacement".to_string(),
line_range: 1..3,
};
assert_eq!(adjust_range_for_change(15..25, &change), 10..21);
}
#[test]
fn test_calculate_line_range_edge_cases() {
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";
assert_eq!(calculate_line_range(source, 0..17), 1..3);
let source = "line1\n测试\nline3";
assert_eq!(calculate_line_range(source, 6..12), 2..2);
}
}