use super::{EditorDocument, Position, Range, Result, StyleBuilder};
use crate::commands::{
AdjustKaraokeCommand, ApplyKaraokeCommand, ApplyStyleCommand, CloneStyleCommand,
CreateStyleCommand, DeleteStyleCommand, EditStyleCommand, EditorCommand, EffectOperation,
EventEffectCommand, GenerateKaraokeCommand, InsertTagCommand, KaraokeType, MergeEventsCommand,
ParseTagCommand, ParsedTag, RemoveTagCommand, ReplaceTagCommand, SplitEventCommand,
SplitKaraokeCommand, TimingAdjustCommand, ToggleEventTypeCommand, WrapTagCommand,
};
use crate::core::errors::EditorError;
use ass_core::parser::ast::{Event, EventType, Section};
use core::cmp::Ordering;
#[cfg(not(feature = "std"))]
use alloc::{
string::{String, ToString},
vec,
vec::Vec,
};
#[cfg(feature = "std")]
use std::vec;
#[derive(Debug, Clone, PartialEq)]
pub struct EventInfo {
pub index: usize,
pub event: OwnedEvent,
pub line_number: usize,
pub range: Range,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OwnedEvent {
pub event_type: EventType,
pub layer: String,
pub start: String,
pub end: String,
pub style: String,
pub name: String,
pub margin_l: String,
pub margin_r: String,
pub margin_v: String,
pub margin_t: Option<String>,
pub margin_b: Option<String>,
pub effect: String,
pub text: String,
}
impl<'a> From<&Event<'a>> for OwnedEvent {
fn from(event: &Event<'a>) -> Self {
Self {
event_type: event.event_type,
layer: event.layer.to_string(),
start: event.start.to_string(),
end: event.end.to_string(),
style: event.style.to_string(),
name: event.name.to_string(),
margin_l: event.margin_l.to_string(),
margin_r: event.margin_r.to_string(),
margin_v: event.margin_v.to_string(),
margin_t: event.margin_t.map(|s| s.to_string()),
margin_b: event.margin_b.map(|s| s.to_string()),
effect: event.effect.to_string(),
text: event.text.to_string(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct EventFilter {
pub event_type: Option<EventType>,
pub style_pattern: Option<String>,
pub speaker_pattern: Option<String>,
pub text_pattern: Option<String>,
pub time_range: Option<(u32, u32)>,
pub layer: Option<u32>,
pub effect_pattern: Option<String>,
pub use_regex: bool,
pub case_sensitive: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EventSortCriteria {
StartTime,
EndTime,
Duration,
Style,
Speaker,
Layer,
Index,
Text,
}
#[derive(Debug, Clone)]
pub struct EventSortOptions {
pub criteria: EventSortCriteria,
pub secondary: Option<EventSortCriteria>,
pub ascending: bool,
}
impl Default for EventSortOptions {
fn default() -> Self {
Self {
criteria: EventSortCriteria::Index,
secondary: None,
ascending: true,
}
}
}
pub struct AtPosition<'a> {
document: &'a mut EditorDocument,
position: Position,
}
impl<'a> AtPosition<'a> {
pub(crate) fn new(document: &'a mut EditorDocument, position: Position) -> Self {
Self { document, position }
}
pub fn insert_text(self, text: &str) -> Result<&'a mut EditorDocument> {
let range = Range::empty(self.position);
self.document.replace(range, text)?;
Ok(self.document)
}
pub fn insert_line(self) -> Result<&'a mut EditorDocument> {
self.insert_text("\n")
}
pub fn delete(self, count: usize) -> Result<&'a mut EditorDocument> {
let end = self.position.advance(count);
let range = Range::new(self.position, end);
self.document.delete(range)?;
Ok(self.document)
}
pub fn backspace(self, count: usize) -> Result<&'a mut EditorDocument> {
let start = self.position.retreat(count);
let range = Range::new(start, self.position);
self.document.delete(range)?;
Ok(self.document)
}
pub fn replace_to_line_end(self, text: &str) -> Result<&'a mut EditorDocument> {
#[cfg(feature = "rope")]
{
let rope = self.document.rope();
let line_idx = rope.byte_to_line(self.position.offset);
let line_end_byte = if line_idx + 1 < rope.len_lines() {
rope.line_to_byte(line_idx + 1).saturating_sub(1)
} else {
rope.len_bytes()
};
let range = Range::new(self.position, Position::new(line_end_byte));
self.document.replace(range, text)?;
Ok(self.document)
}
#[cfg(not(feature = "rope"))]
{
Err(EditorError::FeatureNotEnabled {
feature: "line-based operations".to_string(),
required_feature: "rope".to_string(),
})
}
}
pub const fn position(&self) -> Position {
self.position
}
#[cfg(feature = "rope")]
pub fn to_line_column(&self) -> Result<(usize, usize)> {
let rope = self.document.rope();
let line_idx = rope.byte_to_line(self.position.offset);
let line_start = rope.line_to_byte(line_idx);
let col_offset = self.position.offset - line_start;
let line = rope.line(line_idx);
let mut char_col = 0;
let mut byte_count = 0;
for ch in line.chars() {
if byte_count >= col_offset {
break;
}
byte_count += ch.len_utf8();
char_col += 1;
}
Ok((line_idx + 1, char_col + 1)) }
}
pub struct SelectRange<'a> {
document: &'a mut EditorDocument,
range: Range,
}
impl<'a> SelectRange<'a> {
pub(crate) fn new(document: &'a mut EditorDocument, range: Range) -> Self {
Self { document, range }
}
pub fn replace_with(self, text: &str) -> Result<&'a mut EditorDocument> {
self.document.replace(self.range, text)?;
Ok(self.document)
}
pub fn delete(self) -> Result<&'a mut EditorDocument> {
self.document.delete(self.range)?;
Ok(self.document)
}
pub fn wrap_with_tag(self, open_tag: &str, close_tag: &str) -> Result<&'a mut EditorDocument> {
let selected = self
.document
.rope()
.byte_slice(self.range.start.offset..self.range.end.offset);
let mut wrapped =
String::with_capacity(open_tag.len() + selected.len_bytes() + close_tag.len());
wrapped.push_str(open_tag);
wrapped.push_str(&selected.to_string());
wrapped.push_str(close_tag);
self.document.replace(self.range, &wrapped)?;
Ok(self.document)
}
#[cfg(feature = "rope")]
pub fn indent(self, spaces: usize) -> Result<&'a mut EditorDocument> {
let start_line = self.document.rope().byte_to_line(self.range.start.offset);
let end_line = self.document.rope().byte_to_line(self.range.end.offset);
let indent = " ".repeat(spaces);
let mut line_positions = Vec::new();
for line_idx in (start_line..=end_line).rev() {
let line_start = self.document.rope().line_to_byte(line_idx);
line_positions.push(line_start);
}
for line_start in line_positions {
let pos = Position::new(line_start);
let range = Range::empty(pos);
self.document.replace(range, &indent)?;
}
Ok(self.document)
}
#[cfg(feature = "rope")]
pub fn unindent(self, spaces: usize) -> Result<&'a mut EditorDocument> {
let start_line = self.document.rope().byte_to_line(self.range.start.offset);
let end_line = self.document.rope().byte_to_line(self.range.end.offset);
let mut unindent_ops = Vec::new();
for line_idx in (start_line..=end_line).rev() {
let line_start = self.document.rope().line_to_byte(line_idx);
let line = self.document.rope().line(line_idx);
let mut space_count = 0;
for ch in line.chars().take(spaces) {
if ch == ' ' {
space_count += 1;
} else {
break;
}
}
if space_count > 0 {
unindent_ops.push((line_start, space_count));
}
}
for (line_start, space_count) in unindent_ops {
let range = Range::new(
Position::new(line_start),
Position::new(line_start + space_count),
);
self.document.delete(range)?;
}
Ok(self.document)
}
pub fn text(&self) -> String {
self.document
.rope()
.byte_slice(self.range.start.offset..self.range.end.offset)
.to_string()
}
pub const fn range(&self) -> Range {
self.range
}
}
pub struct StyleOps<'a> {
document: &'a mut EditorDocument,
}
impl<'a> StyleOps<'a> {
pub(crate) fn new(document: &'a mut EditorDocument) -> Self {
Self { document }
}
pub fn create(self, name: &str, builder: StyleBuilder) -> Result<&'a mut EditorDocument> {
let command = CreateStyleCommand::new(name.to_string(), builder);
command.execute(self.document)?;
Ok(self.document)
}
pub fn edit(self, name: &str) -> StyleEditor<'a> {
StyleEditor::new(self.document, name.to_string())
}
pub fn delete(self, name: &str) -> Result<&'a mut EditorDocument> {
let command = DeleteStyleCommand::new(name.to_string());
command.execute(self.document)?;
Ok(self.document)
}
pub fn clone(self, source: &str, target: &str) -> Result<&'a mut EditorDocument> {
let command = CloneStyleCommand::new(source.to_string(), target.to_string());
command.execute(self.document)?;
Ok(self.document)
}
pub fn apply(self, old_style: &str, new_style: &str) -> StyleApplicator<'a> {
StyleApplicator::new(self.document, old_style.to_string(), new_style.to_string())
}
}
pub struct StyleEditor<'a> {
document: &'a mut EditorDocument,
command: EditStyleCommand,
}
impl<'a> StyleEditor<'a> {
pub(crate) fn new(document: &'a mut EditorDocument, style_name: String) -> Self {
let command = EditStyleCommand::new(style_name);
Self { document, command }
}
pub fn font(mut self, font: &str) -> Self {
self.command = self.command.set_font(font);
self
}
pub fn size(mut self, size: u32) -> Self {
self.command = self.command.set_size(size);
self
}
pub fn color(mut self, color: &str) -> Self {
self.command = self.command.set_color(color);
self
}
pub fn bold(mut self, bold: bool) -> Self {
self.command = self.command.set_bold(bold);
self
}
pub fn italic(mut self, italic: bool) -> Self {
self.command = self.command.set_italic(italic);
self
}
pub fn alignment(mut self, alignment: u32) -> Self {
self.command = self.command.set_alignment(alignment);
self
}
pub fn field(mut self, name: &str, value: &str) -> Self {
self.command = self.command.set_field(name, value.to_string());
self
}
pub fn apply(self) -> Result<&'a mut EditorDocument> {
self.command.execute(self.document)?;
Ok(self.document)
}
}
pub struct StyleApplicator<'a> {
document: &'a mut EditorDocument,
command: ApplyStyleCommand,
}
impl<'a> StyleApplicator<'a> {
pub(crate) fn new(
document: &'a mut EditorDocument,
old_style: String,
new_style: String,
) -> Self {
let command = ApplyStyleCommand::new(old_style, new_style);
Self { document, command }
}
pub fn with_filter(mut self, filter: &str) -> Self {
self.command = self.command.with_filter(filter.to_string());
self
}
pub fn apply(self) -> Result<&'a mut EditorDocument> {
self.command.execute(self.document)?;
Ok(self.document)
}
}
pub struct EventOps<'a> {
document: &'a mut EditorDocument,
}
impl<'a> EventOps<'a> {
pub(crate) fn new(document: &'a mut EditorDocument) -> Self {
Self { document }
}
pub fn split(self, event_index: usize, split_time: &str) -> Result<&'a mut EditorDocument> {
let command = SplitEventCommand::new(event_index, split_time.to_string());
command.execute(self.document)?;
Ok(self.document)
}
pub fn merge(self, first_index: usize, second_index: usize) -> EventMerger<'a> {
EventMerger::new(self.document, first_index, second_index)
}
pub fn timing(self) -> EventTimer<'a> {
EventTimer::new(self.document)
}
pub fn toggle_type(self) -> EventToggler<'a> {
EventToggler::new(self.document)
}
pub fn effects(self) -> EventEffector<'a> {
EventEffector::new(self.document)
}
pub fn get(self, index: usize) -> Result<Option<EventInfo>> {
self.document
.parse_script_with(|script| -> Result<Option<EventInfo>> {
let mut current_index = 0;
for section in script.sections() {
if let Section::Events(events) = section {
for event in events {
if current_index == index {
let event_info = EventInfo {
index,
event: OwnedEvent::from(event),
line_number: self.find_line_number_for_event(event)?,
range: self.find_range_for_event(event)?,
};
return Ok(Some(event_info));
}
current_index += 1;
}
}
}
Ok(None)
})?
}
pub fn event(self, index: usize) -> EventAccessor<'a> {
EventAccessor::new(self.document, index)
}
pub fn all(self) -> Result<Vec<EventInfo>> {
EventQuery::new(self.document).execute()
}
pub fn count(self) -> Result<usize> {
self.document.parse_script_with(|script| {
let mut count = 0;
for section in script.sections() {
if let Section::Events(events) = section {
count += events.len();
}
}
count
})
}
pub fn query(self) -> EventQuery<'a> {
EventQuery::new(self.document)
}
pub fn dialogues(self) -> EventQuery<'a> {
EventQuery::new(self.document).filter_by_type(EventType::Dialogue)
}
pub fn comments(self) -> EventQuery<'a> {
EventQuery::new(self.document).filter_by_type(EventType::Comment)
}
pub fn in_time_range(self, start_cs: u32, end_cs: u32) -> EventQuery<'a> {
EventQuery::new(self.document).filter_by_time_range(start_cs, end_cs)
}
pub fn with_style(self, pattern: &str) -> EventQuery<'a> {
EventQuery::new(self.document).filter_by_style(pattern)
}
pub fn containing(self, text: &str) -> EventQuery<'a> {
EventQuery::new(self.document).filter_by_text(text)
}
pub fn in_order(self) -> EventQuery<'a> {
EventQuery::new(self.document).sort(EventSortCriteria::Index)
}
pub fn by_time(self) -> EventQuery<'a> {
EventQuery::new(self.document).sort_by_time()
}
fn find_line_number_for_event(&self, _event: &Event) -> Result<usize> {
Ok(1)
}
fn find_range_for_event(&self, _event: &Event) -> Result<Range> {
Ok(Range::new(Position::new(0), Position::new(0)))
}
}
pub struct EventMerger<'a> {
document: &'a mut EditorDocument,
first_index: usize,
second_index: usize,
separator: String,
}
impl<'a> EventMerger<'a> {
pub(crate) fn new(
document: &'a mut EditorDocument,
first_index: usize,
second_index: usize,
) -> Self {
Self {
document,
first_index,
second_index,
separator: " ".to_string(),
}
}
pub fn with_separator(mut self, separator: &str) -> Self {
self.separator = separator.to_string();
self
}
pub fn apply(self) -> Result<&'a mut EditorDocument> {
let command = MergeEventsCommand::new(self.first_index, self.second_index)
.with_separator(self.separator);
command.execute(self.document)?;
Ok(self.document)
}
}
pub struct EventTimer<'a> {
document: &'a mut EditorDocument,
event_indices: Vec<usize>,
}
impl<'a> EventTimer<'a> {
pub(crate) fn new(document: &'a mut EditorDocument) -> Self {
Self {
document,
event_indices: Vec::new(), }
}
pub fn events(mut self, indices: Vec<usize>) -> Self {
self.event_indices = indices;
self
}
pub fn event(mut self, index: usize) -> Self {
self.event_indices = vec![index];
self
}
pub fn shift(self, offset_cs: i32) -> Result<&'a mut EditorDocument> {
let command = TimingAdjustCommand::new(self.event_indices, offset_cs, offset_cs);
command.execute(self.document)?;
Ok(self.document)
}
pub fn shift_start(self, offset_cs: i32) -> Result<&'a mut EditorDocument> {
let command = TimingAdjustCommand::new(self.event_indices, offset_cs, 0);
command.execute(self.document)?;
Ok(self.document)
}
pub fn shift_end(self, offset_cs: i32) -> Result<&'a mut EditorDocument> {
let command = TimingAdjustCommand::new(self.event_indices, 0, offset_cs);
command.execute(self.document)?;
Ok(self.document)
}
pub fn scale_duration(self, factor: f64) -> Result<&'a mut EditorDocument> {
let command = TimingAdjustCommand::scale_duration(self.event_indices, factor);
command.execute(self.document)?;
Ok(self.document)
}
pub fn adjust(
self,
start_offset_cs: i32,
end_offset_cs: i32,
) -> Result<&'a mut EditorDocument> {
let command = TimingAdjustCommand::new(self.event_indices, start_offset_cs, end_offset_cs);
command.execute(self.document)?;
Ok(self.document)
}
}
pub struct EventToggler<'a> {
document: &'a mut EditorDocument,
event_indices: Vec<usize>,
}
impl<'a> EventToggler<'a> {
pub(crate) fn new(document: &'a mut EditorDocument) -> Self {
Self {
document,
event_indices: Vec::new(), }
}
pub fn events(mut self, indices: Vec<usize>) -> Self {
self.event_indices = indices;
self
}
pub fn event(mut self, index: usize) -> Self {
self.event_indices = vec![index];
self
}
pub fn apply(self) -> Result<&'a mut EditorDocument> {
let command = ToggleEventTypeCommand::new(self.event_indices);
command.execute(self.document)?;
Ok(self.document)
}
}
pub struct EventEffector<'a> {
document: &'a mut EditorDocument,
event_indices: Vec<usize>,
}
impl<'a> EventEffector<'a> {
pub(crate) fn new(document: &'a mut EditorDocument) -> Self {
Self {
document,
event_indices: Vec::new(), }
}
pub fn events(mut self, indices: Vec<usize>) -> Self {
self.event_indices = indices;
self
}
pub fn event(mut self, index: usize) -> Self {
self.event_indices = vec![index];
self
}
pub fn set(self, effect: &str) -> Result<&'a mut EditorDocument> {
let command = EventEffectCommand::set_effect(self.event_indices, effect.to_string());
command.execute(self.document)?;
Ok(self.document)
}
pub fn clear(self) -> Result<&'a mut EditorDocument> {
let command = EventEffectCommand::clear_effect(self.event_indices);
command.execute(self.document)?;
Ok(self.document)
}
pub fn append(self, effect: &str) -> Result<&'a mut EditorDocument> {
let command = EventEffectCommand::append_effect(self.event_indices, effect.to_string());
command.execute(self.document)?;
Ok(self.document)
}
pub fn prepend(self, effect: &str) -> Result<&'a mut EditorDocument> {
let command = EventEffectCommand::new(
self.event_indices,
effect.to_string(),
EffectOperation::Prepend,
);
command.execute(self.document)?;
Ok(self.document)
}
}
pub struct TagOps<'a> {
document: &'a mut EditorDocument,
range: Option<Range>,
position: Option<Position>,
}
impl<'a> TagOps<'a> {
fn new(document: &'a mut EditorDocument) -> Self {
Self {
document,
range: None,
position: None,
}
}
#[must_use]
pub fn at(mut self, position: Position) -> Self {
self.position = Some(position);
self
}
#[must_use]
pub fn in_range(mut self, range: Range) -> Self {
self.range = Some(range);
self
}
pub fn insert(self, tag: &str) -> Result<&'a mut EditorDocument> {
let position = self
.position
.ok_or_else(|| EditorError::command_failed("Position required for tag insertion"))?;
let command = InsertTagCommand::new(position, tag.to_string());
command.execute(self.document)?;
Ok(self.document)
}
pub fn insert_raw(self, tag: &str) -> Result<&'a mut EditorDocument> {
let position = self
.position
.ok_or_else(|| EditorError::command_failed("Position required for tag insertion"))?;
let command = InsertTagCommand::new(position, tag.to_string()).no_auto_wrap();
command.execute(self.document)?;
Ok(self.document)
}
pub fn remove_all(self) -> Result<&'a mut EditorDocument> {
let range = self
.range
.ok_or_else(|| EditorError::command_failed("Range required for tag removal"))?;
let command = RemoveTagCommand::new(range);
command.execute(self.document)?;
Ok(self.document)
}
pub fn remove_pattern(self, pattern: &str) -> Result<&'a mut EditorDocument> {
let range = self
.range
.ok_or_else(|| EditorError::command_failed("Range required for tag removal"))?;
let command = RemoveTagCommand::new(range).pattern(pattern.to_string());
command.execute(self.document)?;
Ok(self.document)
}
pub fn replace(self, find_pattern: &str, replace_with: &str) -> Result<&'a mut EditorDocument> {
let range = self
.range
.ok_or_else(|| EditorError::command_failed("Range required for tag replacement"))?;
let command =
ReplaceTagCommand::new(range, find_pattern.to_string(), replace_with.to_string());
command.execute(self.document)?;
Ok(self.document)
}
pub fn replace_all(
self,
find_pattern: &str,
replace_with: &str,
) -> Result<&'a mut EditorDocument> {
let range = self
.range
.ok_or_else(|| EditorError::command_failed("Range required for tag replacement"))?;
let command =
ReplaceTagCommand::new(range, find_pattern.to_string(), replace_with.to_string()).all();
command.execute(self.document)?;
Ok(self.document)
}
pub fn wrap(self, opening_tag: &str) -> Result<&'a mut EditorDocument> {
let range = self
.range
.ok_or_else(|| EditorError::command_failed("Range required for tag wrapping"))?;
let command = WrapTagCommand::new(range, opening_tag.to_string());
command.execute(self.document)?;
Ok(self.document)
}
pub fn wrap_with(self, opening_tag: &str, closing_tag: &str) -> Result<&'a mut EditorDocument> {
let range = self
.range
.ok_or_else(|| EditorError::command_failed("Range required for tag wrapping"))?;
let command = WrapTagCommand::new(range, opening_tag.to_string())
.closing_tag(closing_tag.to_string());
command.execute(self.document)?;
Ok(self.document)
}
pub fn parse(self) -> Result<Vec<ParsedTag>> {
let range = self.range.unwrap_or_else(|| {
Range::new(Position::new(0), Position::new(self.document.text().len()))
});
let command = ParseTagCommand::new(range).with_positions();
let text = self.document.text_range(range)?;
command.parse_tags_from_text(&text)
}
}
pub struct KaraokeOps<'a> {
document: &'a mut EditorDocument,
range: Option<Range>,
}
impl<'a> KaraokeOps<'a> {
fn new(document: &'a mut EditorDocument) -> Self {
Self {
document,
range: None,
}
}
#[must_use]
pub fn in_range(mut self, range: Range) -> Self {
self.range = Some(range);
self
}
pub fn generate(self, default_duration: u32) -> KaraokeGenerator<'a> {
let default_range = if self.range.is_none() {
let doc_len = self.document.text().len();
Range::new(Position::new(0), Position::new(doc_len))
} else {
Range::new(Position::new(0), Position::new(0)) };
KaraokeGenerator {
document: self.document,
range: self.range.unwrap_or(default_range),
default_duration,
karaoke_type: KaraokeType::Standard,
auto_detect_syllables: true,
}
}
pub fn split(self, split_positions: Vec<usize>) -> KaraokeSplitter<'a> {
let default_range = if self.range.is_none() {
let doc_len = self.document.text().len();
Range::new(Position::new(0), Position::new(doc_len))
} else {
Range::new(Position::new(0), Position::new(0)) };
KaraokeSplitter {
document: self.document,
range: self.range.unwrap_or(default_range),
split_positions,
new_duration: None,
}
}
pub fn adjust(self) -> KaraokeAdjuster<'a> {
let default_range = if self.range.is_none() {
let doc_len = self.document.text().len();
Range::new(Position::new(0), Position::new(doc_len))
} else {
Range::new(Position::new(0), Position::new(0)) };
KaraokeAdjuster {
document: self.document,
range: self.range.unwrap_or(default_range),
}
}
pub fn apply(self) -> KaraokeApplicator<'a> {
let default_range = if self.range.is_none() {
let doc_len = self.document.text().len();
Range::new(Position::new(0), Position::new(doc_len))
} else {
Range::new(Position::new(0), Position::new(0)) };
KaraokeApplicator {
document: self.document,
range: self.range.unwrap_or(default_range),
}
}
}
pub struct KaraokeGenerator<'a> {
document: &'a mut EditorDocument,
range: Range,
default_duration: u32,
karaoke_type: KaraokeType,
auto_detect_syllables: bool,
}
impl<'a> KaraokeGenerator<'a> {
#[must_use]
pub fn karaoke_type(mut self, karaoke_type: KaraokeType) -> Self {
self.karaoke_type = karaoke_type;
self
}
#[must_use]
pub fn manual_syllables(mut self) -> Self {
self.auto_detect_syllables = false;
self
}
pub fn execute(self) -> Result<&'a mut EditorDocument> {
let mut command = GenerateKaraokeCommand::new(self.range, self.default_duration)
.karaoke_type(self.karaoke_type);
if !self.auto_detect_syllables {
command = command.manual_syllables();
}
command.execute(self.document)?;
Ok(self.document)
}
}
pub struct KaraokeSplitter<'a> {
document: &'a mut EditorDocument,
range: Range,
split_positions: Vec<usize>,
new_duration: Option<u32>,
}
impl<'a> KaraokeSplitter<'a> {
#[must_use]
pub fn duration(mut self, duration: u32) -> Self {
self.new_duration = Some(duration);
self
}
pub fn execute(self) -> Result<&'a mut EditorDocument> {
let mut command = SplitKaraokeCommand::new(self.range, self.split_positions);
if let Some(duration) = self.new_duration {
command = command.duration(duration);
}
command.execute(self.document)?;
Ok(self.document)
}
}
pub struct KaraokeAdjuster<'a> {
document: &'a mut EditorDocument,
range: Range,
}
impl<'a> KaraokeAdjuster<'a> {
pub fn scale(self, factor: f32) -> Result<&'a mut EditorDocument> {
let command = AdjustKaraokeCommand::scale(self.range, factor);
command.execute(self.document)?;
Ok(self.document)
}
pub fn offset(self, offset: i32) -> Result<&'a mut EditorDocument> {
let command = AdjustKaraokeCommand::offset(self.range, offset);
command.execute(self.document)?;
Ok(self.document)
}
pub fn set_all(self, duration: u32) -> Result<&'a mut EditorDocument> {
let command = AdjustKaraokeCommand::set_all(self.range, duration);
command.execute(self.document)?;
Ok(self.document)
}
pub fn custom(self, timings: Vec<u32>) -> Result<&'a mut EditorDocument> {
let command = AdjustKaraokeCommand::custom(self.range, timings);
command.execute(self.document)?;
Ok(self.document)
}
}
pub struct KaraokeApplicator<'a> {
document: &'a mut EditorDocument,
range: Range,
}
impl<'a> KaraokeApplicator<'a> {
pub fn equal(self, duration: u32, karaoke_type: KaraokeType) -> Result<&'a mut EditorDocument> {
let command = ApplyKaraokeCommand::equal(self.range, duration, karaoke_type);
command.execute(self.document)?;
Ok(self.document)
}
pub fn beat(
self,
bpm: u32,
beats_per_syllable: f32,
karaoke_type: KaraokeType,
) -> Result<&'a mut EditorDocument> {
let command = ApplyKaraokeCommand::beat(self.range, bpm, beats_per_syllable, karaoke_type);
command.execute(self.document)?;
Ok(self.document)
}
pub fn pattern(
self,
durations: Vec<u32>,
karaoke_type: KaraokeType,
) -> Result<&'a mut EditorDocument> {
let command = ApplyKaraokeCommand::pattern(self.range, durations, karaoke_type);
command.execute(self.document)?;
Ok(self.document)
}
pub fn import_from(self, source_event_index: usize) -> Result<&'a mut EditorDocument> {
let command = ApplyKaraokeCommand::import_from(self.range, source_event_index);
command.execute(self.document)?;
Ok(self.document)
}
}
impl EditorDocument {
pub fn at_pos(&mut self, position: Position) -> AtPosition<'_> {
AtPosition::new(self, position)
}
#[cfg(feature = "rope")]
pub fn at_line(&mut self, line: usize) -> Result<AtPosition<'_>> {
let line_idx = line.saturating_sub(1);
if line_idx >= self.rope().len_lines() {
return Err(EditorError::InvalidPosition { line, column: 1 });
}
let byte_pos = self.rope().line_to_byte(line_idx);
Ok(AtPosition::new(self, Position::new(byte_pos)))
}
pub fn at_start(&mut self) -> AtPosition<'_> {
AtPosition::new(self, Position::start())
}
pub fn at_end(&mut self) -> AtPosition<'_> {
let end_pos = Position::new(self.len());
AtPosition::new(self, end_pos)
}
pub fn select(&mut self, range: Range) -> SelectRange<'_> {
SelectRange::new(self, range)
}
pub fn styles(&mut self) -> StyleOps<'_> {
StyleOps::new(self)
}
pub fn events(&mut self) -> EventOps<'_> {
EventOps::new(self)
}
pub fn tags(&mut self) -> TagOps<'_> {
TagOps::new(self)
}
pub fn karaoke(&mut self) -> KaraokeOps<'_> {
KaraokeOps::new(self)
}
#[cfg(feature = "rope")]
pub fn position_to_line_col(&self, pos: Position) -> Result<(usize, usize)> {
if pos.offset > self.len() {
return Err(EditorError::PositionOutOfBounds {
position: pos.offset,
length: self.len(),
});
}
let line_idx = self.rope().byte_to_line(pos.offset);
let line_start = self.rope().line_to_byte(line_idx);
let col_offset = pos.offset - line_start;
let line = self.rope().line(line_idx);
let mut char_col = 0;
let mut byte_count = 0;
for ch in line.chars() {
if byte_count >= col_offset {
break;
}
byte_count += ch.len_utf8();
char_col += 1;
}
Ok((line_idx + 1, char_col + 1)) }
#[cfg(feature = "rope")]
pub fn line_column_to_position(&self, line: usize, column: usize) -> Result<Position> {
use super::PositionBuilder;
PositionBuilder::new()
.line(line)
.column(column)
.build(self.rope())
}
}
pub struct EventAccessor<'a> {
document: &'a mut EditorDocument,
index: usize,
}
impl<'a> EventAccessor<'a> {
pub(crate) fn new(document: &'a mut EditorDocument, index: usize) -> Self {
Self { document, index }
}
pub fn get(self) -> Result<Option<EventInfo>> {
EventOps::new(self.document).get(self.index)
}
pub fn text(self) -> Result<Option<String>> {
Ok(self.get()?.map(|info| info.event.text))
}
pub fn style(self) -> Result<Option<String>> {
Ok(self.get()?.map(|info| info.event.style))
}
pub fn speaker(self) -> Result<Option<String>> {
Ok(self.get()?.map(|info| info.event.name))
}
pub fn timing(self) -> Result<Option<(String, String)>> {
Ok(self.get()?.map(|info| (info.event.start, info.event.end)))
}
pub fn start_time(self) -> Result<Option<String>> {
Ok(self.get()?.map(|info| info.event.start))
}
pub fn end_time(self) -> Result<Option<String>> {
Ok(self.get()?.map(|info| info.event.end))
}
pub fn layer(self) -> Result<Option<String>> {
Ok(self.get()?.map(|info| info.event.layer))
}
pub fn effect(self) -> Result<Option<String>> {
Ok(self.get()?.map(|info| info.event.effect))
}
pub fn event_type(self) -> Result<Option<EventType>> {
Ok(self.get()?.map(|info| info.event.event_type))
}
pub fn exists(self) -> Result<bool> {
Ok(self.get()?.is_some())
}
pub fn margins(self) -> Result<Option<(String, String, String)>> {
Ok(self.get()?.map(|info| {
(
info.event.margin_l,
info.event.margin_r,
info.event.margin_v,
)
}))
}
pub fn timing_ops(self) -> EventTimer<'a> {
EventTimer::new(self.document).event(self.index)
}
pub fn toggle_ops(self) -> EventToggler<'a> {
EventToggler::new(self.document).event(self.index)
}
pub fn effect_ops(self) -> EventEffector<'a> {
EventEffector::new(self.document).event(self.index)
}
}
pub struct EventQuery<'a> {
document: &'a mut EditorDocument,
filters: EventFilter,
sort_options: Option<EventSortOptions>,
limit: Option<usize>,
}
impl<'a> EventQuery<'a> {
pub(crate) fn new(document: &'a mut EditorDocument) -> Self {
Self {
document,
filters: EventFilter::default(),
sort_options: None,
limit: None,
}
}
pub fn filter(mut self, filter: EventFilter) -> Self {
self.filters = filter;
self
}
pub fn filter_by_type(mut self, event_type: EventType) -> Self {
self.filters.event_type = Some(event_type);
self
}
pub fn filter_by_style(mut self, pattern: &str) -> Self {
self.filters.style_pattern = Some(pattern.to_string());
self
}
pub fn filter_by_speaker(mut self, pattern: &str) -> Self {
self.filters.speaker_pattern = Some(pattern.to_string());
self
}
pub fn filter_by_text(mut self, pattern: &str) -> Self {
self.filters.text_pattern = Some(pattern.to_string());
self
}
pub fn filter_by_time_range(mut self, start_cs: u32, end_cs: u32) -> Self {
self.filters.time_range = Some((start_cs, end_cs));
self
}
pub fn filter_by_layer(mut self, layer: u32) -> Self {
self.filters.layer = Some(layer);
self
}
pub fn filter_by_effect(mut self, pattern: &str) -> Self {
self.filters.effect_pattern = Some(pattern.to_string());
self
}
pub fn with_regex(mut self, use_regex: bool) -> Self {
self.filters.use_regex = use_regex;
self
}
pub fn case_sensitive(mut self, case_sensitive: bool) -> Self {
self.filters.case_sensitive = case_sensitive;
self
}
pub fn sort(mut self, criteria: EventSortCriteria) -> Self {
self.sort_options = Some(EventSortOptions {
criteria,
secondary: None,
ascending: true,
});
self
}
pub fn sort_by(mut self, options: EventSortOptions) -> Self {
self.sort_options = Some(options);
self
}
pub fn sort_by_time(self) -> Self {
self.sort(EventSortCriteria::StartTime)
}
pub fn sort_by_style(self) -> Self {
self.sort(EventSortCriteria::Style)
}
pub fn sort_by_duration(self) -> Self {
self.sort(EventSortCriteria::Duration)
}
pub fn descending(mut self) -> Self {
if let Some(ref mut options) = self.sort_options {
options.ascending = false;
}
self
}
pub fn then_by(mut self, criteria: EventSortCriteria) -> Self {
if let Some(ref mut options) = self.sort_options {
options.secondary = Some(criteria);
}
self
}
pub fn limit(mut self, count: usize) -> Self {
self.limit = Some(count);
self
}
pub fn take(self, count: usize) -> Self {
self.limit(count)
}
pub fn execute(self) -> Result<Vec<EventInfo>> {
let mut results = self.collect_events()?;
results = self.apply_filters(results)?;
if let Some(ref sort_options) = self.sort_options {
self.apply_sort(&mut results, sort_options);
}
if let Some(limit) = self.limit {
results.truncate(limit);
}
Ok(results)
}
pub fn indices(self) -> Result<Vec<usize>> {
Ok(self.execute()?.into_iter().map(|info| info.index).collect())
}
pub fn with_indices(self) -> Result<Vec<(usize, OwnedEvent)>> {
Ok(self
.execute()?
.into_iter()
.map(|info| (info.index, info.event))
.collect())
}
pub fn first(self) -> Result<Option<EventInfo>> {
let mut results = self.limit(1).execute()?;
Ok(results.pop())
}
pub fn count(self) -> Result<usize> {
Ok(self.execute()?.len())
}
pub fn timing(self) -> Result<EventTimer<'a>> {
let _indices: Vec<usize> = self.execute()?.into_iter().map(|info| info.index).collect();
Err(EditorError::command_failed(
"Cannot chain timing operations after query execution - use indices() first",
))
}
pub fn toggle_type(self) -> Result<EventToggler<'a>> {
let _indices: Vec<usize> = self.execute()?.into_iter().map(|info| info.index).collect();
Err(EditorError::command_failed(
"Cannot chain toggle operations after query execution - use indices() first",
))
}
pub fn effects(self) -> Result<EventEffector<'a>> {
let _indices: Vec<usize> = self.execute()?.into_iter().map(|info| info.index).collect();
Err(EditorError::command_failed(
"Cannot chain effect operations after query execution - use indices() first",
))
}
fn collect_events(&self) -> Result<Vec<EventInfo>> {
self.document
.parse_script_with(|script| -> Result<Vec<EventInfo>> {
let mut events = Vec::new();
let mut event_index = 0;
for section in script.sections() {
if let Section::Events(section_events) = section {
for event in section_events {
let event_info = EventInfo {
index: event_index,
event: OwnedEvent::from(event),
line_number: self.find_line_number(event)?,
range: self.find_event_range(event)?,
};
events.push(event_info);
event_index += 1;
}
}
}
Ok(events)
})?
}
fn apply_filters(&self, events: Vec<EventInfo>) -> Result<Vec<EventInfo>> {
let mut filtered = Vec::new();
for event_info in events {
if self.matches_filter(&event_info)? {
filtered.push(event_info);
}
}
Ok(filtered)
}
fn matches_filter(&self, event_info: &EventInfo) -> Result<bool> {
if let Some(event_type) = self.filters.event_type {
if event_info.event.event_type != event_type {
return Ok(false);
}
}
if let Some(ref pattern) = self.filters.style_pattern {
if !self.matches_pattern(&event_info.event.style, pattern)? {
return Ok(false);
}
}
if let Some(ref pattern) = self.filters.text_pattern {
if !self.matches_pattern(&event_info.event.text, pattern)? {
return Ok(false);
}
}
if let Some(ref pattern) = self.filters.speaker_pattern {
if !self.matches_pattern(&event_info.event.name, pattern)? {
return Ok(false);
}
}
if let Some(ref pattern) = self.filters.effect_pattern {
if !self.matches_pattern(&event_info.event.effect, pattern)? {
return Ok(false);
}
}
if let Some(layer) = self.filters.layer {
if let Ok(event_layer) = event_info.event.layer.parse::<u32>() {
if event_layer != layer {
return Ok(false);
}
} else {
return Ok(false);
}
}
if let Some((start_cs, end_cs)) = self.filters.time_range {
if let (Ok(event_start), Ok(event_end)) = (
self.parse_time_to_cs(&event_info.event.start),
self.parse_time_to_cs(&event_info.event.end),
) {
if event_start < start_cs || event_end > end_cs {
return Ok(false);
}
} else {
return Ok(false);
}
}
Ok(true)
}
fn matches_pattern(&self, text: &str, pattern: &str) -> Result<bool> {
if self.filters.use_regex {
Ok(if self.filters.case_sensitive {
text.contains(pattern)
} else {
text.to_lowercase().contains(&pattern.to_lowercase())
})
} else {
Ok(if self.filters.case_sensitive {
text.contains(pattern)
} else {
text.to_lowercase().contains(&pattern.to_lowercase())
})
}
}
fn parse_time_to_cs(&self, time_str: &str) -> Result<u32> {
let parts: Vec<&str> = time_str.split(':').collect();
if parts.len() != 3 {
return Err(EditorError::command_failed("Invalid time format"));
}
let hours: u32 = parts[0]
.parse()
.map_err(|_| EditorError::command_failed("Invalid hours"))?;
let minutes: u32 = parts[1]
.parse()
.map_err(|_| EditorError::command_failed("Invalid minutes"))?;
let sec_cs_parts: Vec<&str> = parts[2].split('.').collect();
if sec_cs_parts.len() != 2 {
return Err(EditorError::command_failed("Invalid seconds format"));
}
let seconds: u32 = sec_cs_parts[0]
.parse()
.map_err(|_| EditorError::command_failed("Invalid seconds"))?;
let centiseconds: u32 = sec_cs_parts[1]
.parse()
.map_err(|_| EditorError::command_failed("Invalid centiseconds"))?;
Ok(hours * 360000 + minutes * 6000 + seconds * 100 + centiseconds)
}
fn apply_sort(&self, events: &mut [EventInfo], options: &EventSortOptions) {
events.sort_by(|a, b| {
let primary_cmp = self.compare_by_criteria(a, b, &options.criteria);
match primary_cmp {
Ordering::Equal => {
if let Some(secondary) = &options.secondary {
let secondary_cmp = self.compare_by_criteria(a, b, secondary);
if options.ascending {
secondary_cmp
} else {
secondary_cmp.reverse()
}
} else {
Ordering::Equal
}
}
other => {
if options.ascending {
other
} else {
other.reverse()
}
}
}
});
}
fn compare_by_criteria(
&self,
a: &EventInfo,
b: &EventInfo,
criteria: &EventSortCriteria,
) -> Ordering {
match criteria {
EventSortCriteria::StartTime => {
let a_time = self.parse_time_to_cs(&a.event.start).unwrap_or(0);
let b_time = self.parse_time_to_cs(&b.event.start).unwrap_or(0);
a_time.cmp(&b_time)
}
EventSortCriteria::EndTime => {
let a_time = self.parse_time_to_cs(&a.event.end).unwrap_or(0);
let b_time = self.parse_time_to_cs(&b.event.end).unwrap_or(0);
a_time.cmp(&b_time)
}
EventSortCriteria::Duration => {
let a_start = self.parse_time_to_cs(&a.event.start).unwrap_or(0);
let a_end = self.parse_time_to_cs(&a.event.end).unwrap_or(0);
let b_start = self.parse_time_to_cs(&b.event.start).unwrap_or(0);
let b_end = self.parse_time_to_cs(&b.event.end).unwrap_or(0);
let a_duration = a_end.saturating_sub(a_start);
let b_duration = b_end.saturating_sub(b_start);
a_duration.cmp(&b_duration)
}
EventSortCriteria::Style => a.event.style.cmp(&b.event.style),
EventSortCriteria::Speaker => a.event.name.cmp(&b.event.name),
EventSortCriteria::Layer => {
let a_layer = a.event.layer.parse::<u32>().unwrap_or(0);
let b_layer = b.event.layer.parse::<u32>().unwrap_or(0);
a_layer.cmp(&b_layer)
}
EventSortCriteria::Index => a.index.cmp(&b.index),
EventSortCriteria::Text => a.event.text.cmp(&b.event.text),
}
}
fn find_line_number(&self, _event: &Event) -> Result<usize> {
Ok(1)
}
fn find_event_range(&self, _event: &Event) -> Result<Range> {
Ok(Range::new(Position::new(0), Position::new(0)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
use alloc::string::ToString;
#[cfg(not(feature = "std"))]
use alloc::vec;
#[test]
#[cfg(feature = "rope")]
fn test_fluent_insert() {
let mut doc = EditorDocument::new();
doc.at_start().insert_text("Hello, ").unwrap();
doc.at_end().insert_text("World!").unwrap();
assert_eq!(doc.text(), "Hello, World!");
}
#[test]
#[cfg(feature = "rope")]
fn test_fluent_line_operations() {
let mut doc = EditorDocument::from_content("Line 1\nLine 2\nLine 3").unwrap();
doc.at_line(2).unwrap().insert_text("Start: ").unwrap();
assert_eq!(doc.text(), "Line 1\nStart: Line 2\nLine 3");
doc.at_line(2)
.unwrap()
.replace_to_line_end("New Line 2")
.unwrap();
assert_eq!(doc.text(), "Line 1\nNew Line 2\nLine 3");
}
#[test]
#[cfg(feature = "rope")]
fn test_fluent_selection() {
let mut doc = EditorDocument::from_content("Hello World").unwrap();
let range = Range::new(Position::new(6), Position::new(11));
doc.select(range).replace_with("Rust").unwrap();
assert_eq!(doc.text(), "Hello Rust");
let range = Range::new(Position::new(6), Position::new(10));
doc.select(range).wrap_with_tag("{\\b1}", "{\\b0}").unwrap();
assert_eq!(doc.text(), "Hello {\\b1}Rust{\\b0}");
}
#[test]
#[cfg(feature = "rope")]
fn test_position_conversion() {
let doc = EditorDocument::from_content("Line 1\nLine 2\nLine 3").unwrap();
let pos = Position::new(7); let (line, col) = doc.position_to_line_col(pos).unwrap();
assert_eq!((line, col), (2, 1));
let pos2 = doc.line_column_to_position(2, 1).unwrap();
assert_eq!(pos2.offset, 7);
}
#[test]
#[cfg(feature = "rope")]
fn test_indent_unindent() {
let mut doc = EditorDocument::from_content("Line 1\nLine 2\nLine 3").unwrap();
let range = Range::new(Position::start(), Position::new(doc.len()));
doc.select(range).indent(2).unwrap();
assert_eq!(doc.text(), " Line 1\n Line 2\n Line 3");
let range = Range::new(Position::start(), Position::new(doc.len()));
doc.select(range).unindent(2).unwrap();
assert_eq!(doc.text(), "Line 1\nLine 2\nLine 3");
}
#[test]
fn test_fluent_style_operations() {
const TEST_CONTENT: &str = 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
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.00,0:00:05.00,Default,Speaker,0,0,0,,Hello world!
"#;
let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
doc.styles()
.create(
"NewStyle",
StyleBuilder::new()
.font("Comic Sans MS")
.size(24)
.bold(true),
)
.unwrap();
assert!(doc.text().contains("Style: NewStyle"));
assert!(doc.text().contains("Comic Sans MS"));
doc.styles()
.edit("Default")
.font("Helvetica")
.size(18)
.bold(true)
.apply()
.unwrap();
assert!(doc.text().contains("Helvetica"));
assert!(doc.text().contains("18"));
doc.styles().clone("Default", "DefaultCopy").unwrap();
assert!(doc.text().contains("Style: DefaultCopy"));
doc.styles().apply("Default", "NewStyle").apply().unwrap();
let text = doc.text();
let events_section = text.split("[Events]").nth(1).unwrap();
assert!(events_section.contains("NewStyle"));
}
#[test]
fn test_fluent_style_delete() {
const TEST_CONTENT: &str = 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: ToDelete,Times,22,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
"#;
let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
assert!(doc.text().contains("Style: ToDelete"));
doc.styles().delete("ToDelete").unwrap();
assert!(!doc.text().contains("Style: ToDelete"));
assert!(doc.text().contains("Style: Default")); }
#[test]
fn test_fluent_style_apply_with_filter() {
const TEST_CONTENT: &str = 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: FilterStyle,Times,22,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.00,0:00:05.00,Default,Speaker,0,0,0,,Hello world!
Dialogue: 0,0:00:06.00,0:00:10.00,Default,Speaker,0,0,0,,Goodbye world!
"#;
let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
doc.styles()
.apply("Default", "FilterStyle")
.with_filter("Hello")
.apply()
.unwrap();
let content = doc.text();
let lines: Vec<&str> = content.lines().collect();
let hello_line = lines.iter().find(|line| line.contains("Hello")).unwrap();
let goodbye_line = lines.iter().find(|line| line.contains("Goodbye")).unwrap();
assert!(hello_line.contains("FilterStyle"));
assert!(goodbye_line.contains("Default")); }
#[test]
fn test_fluent_event_operations() {
const TEST_CONTENT: &str = 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
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.00,0:00:05.00,Default,Speaker,0,0,0,,First event
Dialogue: 0,0:00:05.00,0:00:10.00,Default,Speaker,0,0,0,,Second event
Comment: 0,0:00:10.00,0:00:15.00,Default,Speaker,0,0,0,,Third event
"#;
let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
doc.events().split(0, "0:00:03.00").unwrap();
let events_count = doc
.text()
.lines()
.filter(|line| line.starts_with("Dialogue:") || line.starts_with("Comment:"))
.count();
assert_eq!(events_count, 4);
assert!(doc.text().contains("0:00:01.00,0:00:03.00"));
assert!(doc.text().contains("0:00:03.00,0:00:05.00"));
}
#[test]
fn test_fluent_event_merge() {
const TEST_CONTENT: &str = r#"[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.00,0:00:05.00,Default,Speaker,0,0,0,,First event
Dialogue: 0,0:00:05.00,0:00:10.00,Default,Speaker,0,0,0,,Second event
Comment: 0,0:00:10.00,0:00:15.00,Default,Speaker,0,0,0,,Third event
"#;
let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
doc.events()
.merge(0, 1)
.with_separator(" | ")
.apply()
.unwrap();
let events_count = doc
.text()
.lines()
.filter(|line| line.starts_with("Dialogue:") || line.starts_with("Comment:"))
.count();
assert_eq!(events_count, 2);
assert!(doc.text().contains("First event | Second event"));
assert!(doc.text().contains("0:00:01.00,0:00:10.00")); }
#[test]
fn test_fluent_event_timing() {
const TEST_CONTENT: &str = r#"[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.00,0:00:05.00,Default,Speaker,0,0,0,,First event
Dialogue: 0,0:00:05.00,0:00:10.00,Default,Speaker,0,0,0,,Second event
"#;
let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
doc.events().timing().shift(200).unwrap();
assert!(doc.text().contains("0:00:03.00,0:00:07.00")); assert!(doc.text().contains("0:00:07.00,0:00:12.00")); }
#[test]
fn test_fluent_event_timing_specific() {
const TEST_CONTENT: &str = r#"[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.00,0:00:05.00,Default,Speaker,0,0,0,,First event
Dialogue: 0,0:00:05.00,0:00:10.00,Default,Speaker,0,0,0,,Second event
"#;
let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
doc.events()
.timing()
.event(0)
.shift_start(100) .unwrap();
assert!(doc.text().contains("0:00:02.00,0:00:05.00")); assert!(doc.text().contains("0:00:05.00,0:00:10.00")); }
#[test]
fn test_fluent_event_toggle() {
const TEST_CONTENT: &str = r#"[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.00,0:00:05.00,Default,Speaker,0,0,0,,First event
Comment: 0,0:00:05.00,0:00:10.00,Default,Speaker,0,0,0,,Second event
"#;
let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
doc.events().toggle_type().event(0).apply().unwrap();
let text = doc.text();
let lines: Vec<&str> = text.lines().collect();
let event_lines: Vec<&str> = lines
.iter()
.filter(|line| line.starts_with("Dialogue:") || line.starts_with("Comment:"))
.copied()
.collect();
assert_eq!(event_lines.len(), 2);
assert!(event_lines[0].starts_with("Comment:"));
assert!(event_lines[1].starts_with("Comment:")); }
#[test]
fn test_fluent_event_effects() {
const TEST_CONTENT: &str = r#"[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.00,0:00:05.00,Default,Speaker,0,0,0,,First event
Dialogue: 0,0:00:05.00,0:00:10.00,Default,Speaker,0,0,0,,Second event
"#;
let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
doc.events()
.effects()
.events(vec![0, 1])
.set("Fade(255,0)")
.unwrap();
let text = doc.text();
let event_lines: Vec<&str> = text
.lines()
.filter(|line| line.starts_with("Dialogue:") || line.starts_with("Comment:"))
.collect();
assert!(event_lines[0].contains("Fade(255,0)"));
assert!(event_lines[1].contains("Fade(255,0)"));
}
#[test]
fn test_fluent_event_effects_chaining() {
const TEST_CONTENT: &str = r#"[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.00,0:00:05.00,Default,Speaker,0,0,0,,First event
"#;
let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
doc.events().effects().event(0).set("Fade(255,0)").unwrap();
doc.events()
.effects()
.event(0)
.append("Move(100,200)")
.unwrap();
assert!(doc.text().contains("Fade(255,0) Move(100,200)"));
doc.events().effects().event(0).clear().unwrap();
let text = doc.text();
let event_line = text
.lines()
.find(|line| line.starts_with("Dialogue:"))
.unwrap();
let parts: Vec<&str> = event_line.split(',').collect();
assert_eq!(parts[8].trim(), ""); }
#[test]
fn test_fluent_event_complex_workflow() {
const TEST_CONTENT: &str = r#"[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.00,0:00:05.00,Default,Speaker,0,0,0,,Long event that needs splitting
Dialogue: 0,0:00:05.00,0:00:07.00,Default,Speaker,0,0,0,,Short event
Comment: 0,0:00:10.00,0:00:15.00,Default,Speaker,0,0,0,,Comment to toggle
"#;
let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
doc.events().split(0, "0:00:03.00").unwrap();
doc.events()
.timing()
.shift(100) .unwrap();
doc.events().toggle_type().event(3).apply().unwrap();
doc.events().effects().set("Fade(255,0)").unwrap();
let content = doc.text();
let event_lines: Vec<&str> = content
.lines()
.filter(|line| line.starts_with("Dialogue:") || line.starts_with("Comment:"))
.collect();
assert_eq!(event_lines.len(), 4);
assert!(event_lines.iter().all(|line| line.starts_with("Dialogue:")));
assert!(content.contains("0:00:02.00,0:00:04.00")); assert!(content.contains("0:00:04.00,0:00:06.00")); assert!(content.contains("0:00:06.00,0:00:08.00")); assert!(content.contains("0:00:11.00,0:00:16.00"));
assert!(event_lines.iter().all(|line| line.contains("Fade(255,0)")));
}
#[test]
fn tag_operations() {
let mut doc = EditorDocument::from_content("Hello World").unwrap();
doc.tags().at(Position::new(5)).insert("\\b1").unwrap();
assert_eq!(doc.text(), "Hello{\\b1} World");
doc.tags().at(Position::new(12)).insert_raw("\\i1").unwrap();
assert_eq!(doc.text(), "Hello{\\b1} W\\i1orld");
}
#[test]
fn tag_removal() {
let mut doc =
EditorDocument::from_content("Hello {\\b1\\i1}World{\\c&H00FF00&} test").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.tags().in_range(range).remove_pattern("\\b").unwrap();
assert_eq!(doc.text(), "Hello {\\i1}World{\\c&H00FF00&} test");
let full_range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.tags().in_range(full_range).remove_all().unwrap();
assert_eq!(doc.text(), "Hello World test");
}
#[test]
fn tag_replacement() {
let mut doc = EditorDocument::from_content("Hello {\\b1}World{\\b1} test").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.tags()
.in_range(range)
.replace_all("\\b1", "\\i1")
.unwrap();
assert_eq!(doc.text(), "Hello {\\i1}World{\\i1} test");
}
#[test]
fn tag_wrapping() {
let mut doc = EditorDocument::from_content("Hello World").unwrap();
let range = Range::new(Position::new(6), Position::new(11));
doc.tags().in_range(range).wrap("\\b1").unwrap();
assert_eq!(doc.text(), "Hello {\\b1}World{\\b0}");
let mut doc2 = EditorDocument::from_content("Hello World").unwrap();
let range2 = Range::new(Position::new(6), Position::new(11));
doc2.tags()
.in_range(range2)
.wrap_with("\\c&HFF0000&", "\\c")
.unwrap();
assert_eq!(doc2.text(), "Hello {\\c&HFF0000&}World{\\c}");
}
#[test]
fn tag_parsing() {
let mut doc =
EditorDocument::from_content("Hello {\\b1\\c&H00FF00&\\pos(100,200)}World").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
let parsed_tags = doc.tags().in_range(range).parse().unwrap();
assert_eq!(parsed_tags.len(), 3);
assert_eq!(parsed_tags[0].tag, "\\b1");
assert_eq!(parsed_tags[1].tag, "\\c&H00FF00&");
assert_eq!(parsed_tags[2].tag, "\\pos");
assert_eq!(parsed_tags[2].parameters.len(), 2);
assert_eq!(parsed_tags[2].parameters[0], "100");
assert_eq!(parsed_tags[2].parameters[1], "200");
}
#[test]
fn karaoke_generate() {
let mut doc = EditorDocument::from_content("Hello World Test").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.karaoke()
.in_range(range)
.generate(50)
.manual_syllables()
.execute()
.unwrap();
let text = doc.text();
assert!(text.contains("\\k50"));
assert!(text.contains("Hello World Test"));
}
#[test]
fn karaoke_generate_with_types() {
let mut doc = EditorDocument::from_content("Test Text").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.karaoke()
.in_range(range)
.generate(40)
.karaoke_type(KaraokeType::Fill)
.execute()
.unwrap();
assert!(doc.text().contains("\\kf40"));
let mut doc2 = EditorDocument::from_content("Test Text").unwrap();
let range2 = Range::new(Position::new(0), Position::new(doc2.text().len()));
doc2.karaoke()
.in_range(range2)
.generate(30)
.karaoke_type(KaraokeType::Outline)
.execute()
.unwrap();
assert!(doc2.text().contains("\\ko30"));
}
#[test]
fn karaoke_generate_manual_syllables() {
let mut doc = EditorDocument::from_content("Syllable Test").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.karaoke()
.in_range(range)
.generate(60)
.manual_syllables()
.execute()
.unwrap();
let text = doc.text();
assert!(text.contains("\\k60"));
assert!(text.contains("Syllable Test"));
}
#[test]
fn karaoke_split() {
let mut doc = EditorDocument::from_content("{\\k100}Hello World").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.karaoke()
.in_range(range)
.split(vec![5])
.duration(25)
.execute()
.unwrap();
let text = doc.text();
assert!(text.contains("\\k25"));
}
#[test]
fn karaoke_adjust_scale() {
let mut doc = EditorDocument::from_content("{\\k50}Hello {\\k30}World").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.karaoke().in_range(range).adjust().scale(2.0).unwrap();
let text = doc.text();
assert!(text.contains("\\k100")); assert!(text.contains("\\k60")); }
#[test]
fn karaoke_adjust_offset() {
let mut doc = EditorDocument::from_content("{\\k50}Hello {\\k30}World").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.karaoke().in_range(range).adjust().offset(20).unwrap();
let text = doc.text();
assert!(text.contains("\\k70")); assert!(text.contains("\\k50")); }
#[test]
fn karaoke_adjust_set_all() {
let mut doc = EditorDocument::from_content("{\\k50}Hello {\\k30}World").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.karaoke().in_range(range).adjust().set_all(45).unwrap();
let text = doc.text();
assert!(text.contains("\\k45"));
assert_eq!(text.matches("\\k45").count(), 2);
}
#[test]
fn karaoke_adjust_custom() {
let mut doc = EditorDocument::from_content("{\\k50}Hello {\\k30}World").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.karaoke()
.in_range(range)
.adjust()
.custom(vec![80, 40])
.unwrap();
let text = doc.text();
assert!(text.contains("\\k80"));
assert!(text.contains("\\k40"));
}
#[test]
fn karaoke_apply_equal() {
let mut doc = EditorDocument::from_content("Hello World Test").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.karaoke()
.in_range(range)
.apply()
.equal(35, KaraokeType::Fill)
.unwrap();
let text = doc.text();
assert!(text.contains("\\kf35"));
assert!(text.contains("Hello"));
assert!(text.contains("World"));
assert!(text.contains("Test"));
}
#[test]
fn karaoke_apply_beat() {
let mut doc = EditorDocument::from_content("Hello World").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.karaoke()
.in_range(range)
.apply()
.beat(120, 0.5, KaraokeType::Standard)
.unwrap();
let text = doc.text();
assert!(text.contains("\\k25"));
}
#[test]
fn karaoke_apply_pattern() {
let mut doc = EditorDocument::from_content("Hello World Test").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.karaoke()
.in_range(range)
.apply()
.pattern(vec![40, 60], KaraokeType::Outline)
.unwrap();
let text = doc.text();
assert!(text.contains("\\ko40"));
assert!(text.contains("\\ko60"));
}
#[test]
fn karaoke_apply_import() {
let mut doc = EditorDocument::from_content("Source text for import").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.karaoke()
.in_range(range)
.apply()
.import_from(0)
.unwrap();
assert!(doc.text().contains("Source text for import"));
}
#[test]
fn karaoke_complex_workflow() {
let mut doc =
EditorDocument::from_content("Complex karaoke test with multiple words").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.karaoke()
.in_range(range)
.generate(50)
.karaoke_type(KaraokeType::Standard)
.manual_syllables()
.execute()
.unwrap();
let mut text = doc.text();
assert!(text.contains("\\k50"));
let current_range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.karaoke()
.in_range(current_range)
.adjust()
.scale(1.5)
.unwrap();
text = doc.text();
assert!(text.contains("\\k75"));
let final_range = Range::new(Position::new(0), Position::new(doc.text().len()));
doc.karaoke()
.in_range(final_range)
.adjust()
.offset(10)
.unwrap();
text = doc.text();
assert!(text.contains("\\k85"));
assert!(text.contains("Complex karaoke test with multiple words"));
}
#[test]
fn karaoke_different_types_workflow() {
let test_text = "Test karaoke types";
let mut doc1 = EditorDocument::from_content(test_text).unwrap();
let range1 = Range::new(Position::new(0), Position::new(doc1.text().len()));
doc1.karaoke()
.in_range(range1)
.generate(30)
.karaoke_type(KaraokeType::Standard)
.execute()
.unwrap();
assert!(doc1.text().contains("\\k30"));
let mut doc2 = EditorDocument::from_content(test_text).unwrap();
let range2 = Range::new(Position::new(0), Position::new(doc2.text().len()));
doc2.karaoke()
.in_range(range2)
.generate(40)
.karaoke_type(KaraokeType::Fill)
.execute()
.unwrap();
assert!(doc2.text().contains("\\kf40"));
let mut doc3 = EditorDocument::from_content(test_text).unwrap();
let range3 = Range::new(Position::new(0), Position::new(doc3.text().len()));
doc3.karaoke()
.in_range(range3)
.generate(50)
.karaoke_type(KaraokeType::Outline)
.execute()
.unwrap();
assert!(doc3.text().contains("\\ko50"));
let mut doc4 = EditorDocument::from_content(test_text).unwrap();
let range4 = Range::new(Position::new(0), Position::new(doc4.text().len()));
doc4.karaoke()
.in_range(range4)
.generate(60)
.karaoke_type(KaraokeType::Transition)
.execute()
.unwrap();
assert!(doc4.text().contains("\\kt60"));
}
#[test]
fn karaoke_error_conditions() {
let mut doc = EditorDocument::from_content("Hello {\\b1}World{\\b0}").unwrap();
let range = Range::new(Position::new(0), Position::new(doc.text().len()));
let result = doc.karaoke().in_range(range).generate(50).execute();
assert!(result.is_err());
}
#[test]
fn karaoke_edge_cases() {
let mut doc = EditorDocument::from_content("").unwrap();
let range = Range::new(Position::new(0), Position::new(0));
let result = doc.karaoke().in_range(range).generate(50).execute();
assert!(result.is_ok());
let mut doc2 = EditorDocument::from_content("A").unwrap();
let range2 = Range::new(Position::new(0), Position::new(1));
doc2.karaoke()
.in_range(range2)
.generate(25)
.execute()
.unwrap();
assert!(doc2.text().contains("\\k25"));
assert!(doc2.text().contains("A"));
}
#[test]
fn test_new_event_api_direct_access() {
const TEST_CONTENT: &str = 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
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.00,0:00:05.00,Default,Speaker,0,0,0,,First event
Dialogue: 0,0:00:05.00,0:00:10.00,Default,Speaker,0,0,0,,Second event
Comment: 0,0:00:10.00,0:00:15.00,Default,Speaker,0,0,0,,Third event
"#;
let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
let event_info = doc.events().get(0).unwrap();
assert!(event_info.is_some());
let info = event_info.unwrap();
assert_eq!(info.index, 0);
assert_eq!(info.event.text, "First event");
assert_eq!(info.event.event_type, EventType::Dialogue);
let count = doc.events().count().unwrap();
assert_eq!(count, 3);
let text = doc.events().event(1).text().unwrap();
assert_eq!(text, Some("Second event".to_string()));
let style = doc.events().event(1).style().unwrap();
assert_eq!(style, Some("Default".to_string()));
let exists = doc.events().event(5).exists().unwrap();
assert!(!exists);
}
#[test]
fn test_new_event_api_filtering() {
const TEST_CONTENT: &str = 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
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:01.00,0:00:05.00,Default,Speaker,0,0,0,,First dialogue
Dialogue: 0,0:00:05.00,0:00:10.00,Default,Speaker,0,0,0,,Second dialogue
Comment: 0,0:00:10.00,0:00:15.00,Default,Speaker,0,0,0,,First comment
Comment: 0,0:00:15.00,0:00:20.00,Default,Speaker,0,0,0,,Second comment
"#;
let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
let dialogues = doc.events().dialogues().execute().unwrap();
assert_eq!(dialogues.len(), 2);
assert!(dialogues
.iter()
.all(|info| info.event.event_type == EventType::Dialogue));
let comments = doc.events().comments().execute().unwrap();
assert_eq!(comments.len(), 2);
assert!(comments
.iter()
.all(|info| info.event.event_type == EventType::Comment));
let with_first = doc
.events()
.query()
.filter_by_text("First")
.execute()
.unwrap();
assert_eq!(with_first.len(), 2);
assert!(with_first[0].event.text.contains("First"));
assert!(with_first[1].event.text.contains("First"));
let with_first_insensitive = doc
.events()
.query()
.filter_by_text("first")
.case_sensitive(false)
.execute()
.unwrap();
assert_eq!(with_first_insensitive.len(), 2);
}
#[test]
fn test_new_event_api_sorting() {
const TEST_CONTENT: &str = 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
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:10.00,0:00:15.00,Default,Speaker,0,0,0,,Third by time
Dialogue: 0,0:00:01.00,0:00:05.00,Default,Speaker,0,0,0,,First by time
Dialogue: 0,0:00:05.00,0:00:10.00,Default,Speaker,0,0,0,,Second by time
"#;
let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
let by_time = doc.events().by_time().execute().unwrap();
assert_eq!(by_time.len(), 3);
assert_eq!(by_time[0].event.text, "First by time");
assert_eq!(by_time[1].event.text, "Second by time");
assert_eq!(by_time[2].event.text, "Third by time");
let in_order = doc.events().in_order().execute().unwrap();
assert_eq!(in_order.len(), 3);
assert_eq!(in_order[0].event.text, "Third by time");
assert_eq!(in_order[1].event.text, "First by time");
assert_eq!(in_order[2].event.text, "Second by time");
let by_time_desc = doc
.events()
.query()
.sort_by_time()
.descending()
.execute()
.unwrap();
assert_eq!(by_time_desc[0].event.text, "Third by time");
assert_eq!(by_time_desc[1].event.text, "Second by time");
assert_eq!(by_time_desc[2].event.text, "First by time");
}
#[test]
fn test_new_event_api_combined_operations() {
const TEST_CONTENT: &str = 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
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:10.00,0:00:15.00,Default,Speaker,0,0,0,,Important dialogue
Dialogue: 0,0:00:01.00,0:00:05.00,Default,Speaker,0,0,0,,Another dialogue
Comment: 0,0:00:05.00,0:00:10.00,Default,Speaker,0,0,0,,Important comment
Dialogue: 0,0:00:15.00,0:00:20.00,Default,Speaker,0,0,0,,Final dialogue
"#;
let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
let important_dialogues = doc
.events()
.query()
.filter_by_type(EventType::Dialogue)
.filter_by_text("Important")
.sort_by_time()
.limit(1)
.execute()
.unwrap();
assert_eq!(important_dialogues.len(), 1);
assert_eq!(important_dialogues[0].event.text, "Important dialogue");
assert_eq!(important_dialogues[0].event.event_type, EventType::Dialogue);
let dialogue_indices = doc.events().dialogues().sort_by_time().indices().unwrap();
assert_eq!(dialogue_indices.len(), 3);
assert_eq!(dialogue_indices, vec![1, 0, 3]);
let dialogue_count = doc.events().dialogues().count().unwrap();
assert_eq!(dialogue_count, 3);
let first_dialogue = doc.events().dialogues().sort_by_time().first().unwrap();
assert!(first_dialogue.is_some());
let first = first_dialogue.unwrap();
assert_eq!(first.event.text, "Another dialogue");
}
#[test]
fn karaoke_chaining_operations() {
let mut doc = EditorDocument::from_content("Chain test").unwrap();
doc.at_pos(Position::new(0))
.insert_text("Prefix: ")
.unwrap();
assert_eq!(doc.text(), "Prefix: Chain test");
let karaoke_range = Range::new(Position::new(8), Position::new(doc.text().len()));
doc.karaoke()
.in_range(karaoke_range)
.generate(45)
.manual_syllables()
.execute()
.unwrap();
let text = doc.text();
assert!(text.starts_with("Prefix: "));
assert!(text.contains("\\k45"));
assert!(text.contains("Chain test"));
}
}