1use crate::{Result, ScriptVersion};
7
8#[cfg(not(feature = "std"))]
9extern crate alloc;
10#[cfg(feature = "stream")]
11use alloc::format;
12use alloc::{
13 boxed::Box,
14 string::{String, ToString},
15 vec,
16 vec::Vec,
17};
18#[cfg(feature = "stream")]
19use core::ops::Range;
20
21#[cfg(feature = "stream")]
22use super::streaming;
23use super::{
24 ast::{Event, Section, SectionType, Style},
25 errors::{ParseError, ParseIssue},
26 main::Parser,
27};
28
29#[cfg(feature = "stream")]
30use super::ast::{Font, Graphic};
31
32#[cfg(feature = "plugins")]
33use crate::plugin::ExtensionRegistry;
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum LineContent<'a> {
38 Style(Box<Style<'a>>),
40 Event(Box<Event<'a>>),
42 Field(&'a str, &'a str),
44}
45
46#[derive(Debug, Clone)]
48pub struct UpdateOperation<'a> {
49 pub offset: usize,
51 pub new_line: &'a str,
53 pub line_number: u32,
55}
56
57#[derive(Debug)]
59pub struct BatchUpdateResult<'a> {
60 pub updated: Vec<(usize, LineContent<'a>)>,
62 pub failed: Vec<(usize, ParseError)>,
64}
65
66#[derive(Debug, Clone)]
68pub struct StyleBatch<'a> {
69 pub styles: Vec<Style<'a>>,
71}
72
73#[derive(Debug, Clone)]
75pub struct EventBatch<'a> {
76 pub events: Vec<Event<'a>>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum Change<'a> {
83 Added {
85 offset: usize,
87 content: LineContent<'a>,
89 line_number: u32,
91 },
92 Removed {
94 offset: usize,
96 section_type: SectionType,
98 line_number: u32,
100 },
101 Modified {
103 offset: usize,
105 old_content: LineContent<'a>,
107 new_content: LineContent<'a>,
109 line_number: u32,
111 },
112 SectionAdded {
114 section: Section<'a>,
116 index: usize,
118 },
119 SectionRemoved {
121 section_type: SectionType,
123 index: usize,
125 },
126}
127
128#[derive(Debug, Default, Clone, PartialEq, Eq)]
130pub struct ChangeTracker<'a> {
131 changes: Vec<Change<'a>>,
133 enabled: bool,
135}
136
137impl<'a> ChangeTracker<'a> {
138 pub fn enable(&mut self) {
140 self.enabled = true;
141 }
142
143 pub fn disable(&mut self) {
145 self.enabled = false;
146 }
147
148 #[must_use]
150 pub const fn is_enabled(&self) -> bool {
151 self.enabled
152 }
153
154 pub fn record(&mut self, change: Change<'a>) {
156 if self.enabled {
157 self.changes.push(change);
158 }
159 }
160
161 #[must_use]
163 pub fn changes(&self) -> &[Change<'a>] {
164 &self.changes
165 }
166
167 pub fn clear(&mut self) {
169 self.changes.clear();
170 }
171
172 #[must_use]
174 pub fn len(&self) -> usize {
175 self.changes.len()
176 }
177
178 #[must_use]
180 pub fn is_empty(&self) -> bool {
181 self.changes.is_empty()
182 }
183}
184
185#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct Script<'a> {
191 source: &'a str,
193
194 version: ScriptVersion,
196
197 sections: Vec<Section<'a>>,
199
200 issues: Vec<ParseIssue>,
202
203 styles_format: Option<Vec<&'a str>>,
205
206 events_format: Option<Vec<&'a str>>,
208
209 change_tracker: ChangeTracker<'a>,
211}
212
213impl<'a> Script<'a> {
214 pub fn parse(source: &'a str) -> Result<Self> {
238 let parser = Parser::new(source);
239 Ok(parser.parse())
240 }
241
242 #[must_use]
265 pub const fn builder() -> ScriptBuilder<'a> {
266 ScriptBuilder::new()
267 }
268
269 #[cfg(feature = "stream")]
288 pub fn parse_partial(&self, range: Range<usize>, new_text: &str) -> Result<ScriptDeltaOwned> {
289 let modified_source =
291 streaming::build_modified_source(self.source, range.clone(), new_text);
292
293 let change = crate::parser::incremental::TextChange {
295 range: range.clone(),
296 new_text: new_text.to_string(),
297 line_range: crate::parser::incremental::calculate_line_range(self.source, range),
298 };
299
300 let new_script = self.parse_incremental(&modified_source, &change)?;
302
303 let delta = calculate_delta(self, &new_script);
305
306 let mut owned_delta = ScriptDeltaOwned {
308 added: Vec::new(),
309 modified: Vec::new(),
310 removed: Vec::new(),
311 new_issues: Vec::new(),
312 };
313
314 for section in delta.added {
316 owned_delta.added.push(format!("{section:?}"));
317 }
318
319 for (idx, section) in delta.modified {
321 owned_delta.modified.push((idx, format!("{section:?}")));
322 }
323
324 owned_delta.removed = delta.removed;
326
327 owned_delta.new_issues = delta.new_issues;
329
330 Ok(owned_delta)
331 }
332
333 #[must_use]
335 pub const fn version(&self) -> ScriptVersion {
336 self.version
337 }
338
339 #[must_use]
341 #[allow(clippy::missing_const_for_fn)]
342 pub fn sections(&self) -> &[Section<'a>] {
343 &self.sections
344 }
345
346 #[must_use]
348 #[allow(clippy::missing_const_for_fn)]
349 pub fn issues(&self) -> &[ParseIssue] {
350 &self.issues
351 }
352
353 #[must_use]
355 pub const fn source(&self) -> &'a str {
356 self.source
357 }
358
359 #[must_use]
361 pub fn styles_format(&self) -> Option<&[&'a str]> {
362 self.styles_format.as_deref()
363 }
364
365 #[must_use]
367 pub fn events_format(&self) -> Option<&[&'a str]> {
368 self.events_format.as_deref()
369 }
370
371 pub fn parse_style_line_with_context(
389 &self,
390 line: &'a str,
391 line_number: u32,
392 ) -> core::result::Result<Style<'a>, ParseError> {
393 use super::sections::StylesParser;
394
395 let format = self.styles_format.as_deref().unwrap_or(&[
396 "Name",
397 "Fontname",
398 "Fontsize",
399 "PrimaryColour",
400 "SecondaryColour",
401 "OutlineColour",
402 "BackColour",
403 "Bold",
404 "Italic",
405 "Underline",
406 "StrikeOut",
407 "ScaleX",
408 "ScaleY",
409 "Spacing",
410 "Angle",
411 "BorderStyle",
412 "Outline",
413 "Shadow",
414 "Alignment",
415 "MarginL",
416 "MarginR",
417 "MarginV",
418 "Encoding",
419 ]);
420
421 StylesParser::parse_style_line(line, format, line_number)
422 }
423
424 pub fn parse_event_line_with_context(
443 &self,
444 line: &'a str,
445 line_number: u32,
446 ) -> core::result::Result<Event<'a>, ParseError> {
447 use super::sections::EventsParser;
448
449 let format = self.events_format.as_deref().unwrap_or(&[
450 "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV", "Effect",
451 "Text",
452 ]);
453
454 EventsParser::parse_event_line(line, format, line_number)
455 }
456
457 pub fn parse_line_auto(
474 &self,
475 line: &'a str,
476 line_number: u32,
477 ) -> core::result::Result<(SectionType, LineContent<'a>), ParseError> {
478 let trimmed = line.trim();
479
480 if trimmed.starts_with("Style:") {
482 if let Some(style_data) = trimmed.strip_prefix("Style:") {
483 let style = self.parse_style_line_with_context(style_data.trim(), line_number)?;
484 return Ok((SectionType::Styles, LineContent::Style(Box::new(style))));
485 }
486 } else if trimmed.starts_with("Dialogue:")
487 || trimmed.starts_with("Comment:")
488 || trimmed.starts_with("Picture:")
489 || trimmed.starts_with("Sound:")
490 || trimmed.starts_with("Movie:")
491 || trimmed.starts_with("Command:")
492 {
493 let event = self.parse_event_line_with_context(trimmed, line_number)?;
494 return Ok((SectionType::Events, LineContent::Event(Box::new(event))));
495 } else if trimmed.contains(':') && !trimmed.starts_with("Format:") {
496 if let Some(colon_pos) = trimmed.find(':') {
498 let key = trimmed[..colon_pos].trim();
499 let value = trimmed[colon_pos + 1..].trim();
500 return Ok((SectionType::ScriptInfo, LineContent::Field(key, value)));
501 }
502 }
503
504 Err(ParseError::InvalidFieldFormat {
505 line: line_number as usize,
506 })
507 }
508
509 #[must_use]
511 pub fn find_section(&self, section_type: SectionType) -> Option<&Section<'a>> {
512 self.sections
513 .iter()
514 .find(|s| s.section_type() == section_type)
515 }
516
517 #[cfg(debug_assertions)]
521 #[must_use]
522 pub fn validate_spans(&self) -> bool {
523 let source_ptr = self.source.as_ptr();
524 let source_range = source_ptr as usize..source_ptr as usize + self.source.len();
525
526 self.sections
527 .iter()
528 .all(|section| section.validate_spans(&source_range))
529 }
530
531 #[must_use]
536 pub fn section_range(&self, section_type: SectionType) -> Option<core::ops::Range<usize>> {
537 self.find_section(section_type)?
538 .span()
539 .map(|s| s.start..s.end)
540 }
541
542 #[must_use]
547 pub fn section_at_offset(&self, offset: usize) -> Option<&Section<'a>> {
548 self.sections.iter().find(|s| {
549 s.span()
550 .is_some_and(|span| span.start <= offset && offset < span.end)
551 })
552 }
553
554 #[must_use]
560 pub fn section_boundaries(&self) -> Vec<(SectionType, core::ops::Range<usize>)> {
561 self.sections
562 .iter()
563 .filter_map(|s| {
564 s.span()
565 .map(|span| (s.section_type(), span.start..span.end))
566 })
567 .collect()
568 }
569
570 pub(super) fn from_parts(
572 source: &'a str,
573 version: ScriptVersion,
574 sections: Vec<Section<'a>>,
575 issues: Vec<ParseIssue>,
576 styles_format: Option<Vec<&'a str>>,
577 events_format: Option<Vec<&'a str>>,
578 ) -> Self {
579 Self {
580 source,
581 version,
582 sections,
583 issues,
584 styles_format,
585 events_format,
586 change_tracker: ChangeTracker::default(),
587 }
588 }
589
590 pub fn update_line_at_offset(
609 &mut self,
610 offset: usize,
611 new_line: &'a str,
612 line_number: u32,
613 ) -> core::result::Result<LineContent<'a>, ParseError> {
614 let section_index = self
616 .sections
617 .iter()
618 .position(|s| {
619 s.span()
620 .is_some_and(|span| span.start <= offset && offset < span.end)
621 })
622 .ok_or(ParseError::SectionNotFound)?;
623
624 let (_, new_content) = self.parse_line_auto(new_line, line_number)?;
626
627 let result = match (&mut self.sections[section_index], new_content.clone()) {
629 (Section::Styles(styles), LineContent::Style(new_style)) => {
630 styles
632 .iter()
633 .position(|s| s.span.start <= offset && offset < s.span.end)
634 .map_or(Err(ParseError::IndexOutOfBounds), |style_index| {
635 let old_style = styles[style_index].clone();
636 styles[style_index] = *new_style;
637 Ok(LineContent::Style(Box::new(old_style)))
638 })
639 }
640 (Section::Events(events), LineContent::Event(new_event)) => {
641 events
643 .iter()
644 .position(|e| e.span.start <= offset && offset < e.span.end)
645 .map_or(Err(ParseError::IndexOutOfBounds), |event_index| {
646 let old_event = events[event_index].clone();
647 events[event_index] = *new_event;
648 Ok(LineContent::Event(Box::new(old_event)))
649 })
650 }
651 (Section::ScriptInfo(info), LineContent::Field(key, value)) => {
652 if let Some(field_index) = info.fields.iter().position(|(k, _)| *k == key) {
654 let old_value = info.fields[field_index].1;
655 info.fields[field_index] = (key, value);
656 Ok(LineContent::Field(key, old_value))
657 } else {
658 info.fields.push((key, value));
660 self.change_tracker.record(Change::Added {
662 offset,
663 content: LineContent::Field(key, value),
664 line_number,
665 });
666 Ok(LineContent::Field(key, ""))
667 }
668 }
669 _ => Err(ParseError::InvalidFieldFormat {
670 line: line_number as usize,
671 }),
672 };
673
674 if let Ok(old_content) = &result {
676 if !matches!(old_content, LineContent::Field(_, "")) {
677 self.change_tracker.record(Change::Modified {
679 offset,
680 old_content: old_content.clone(),
681 new_content,
682 line_number,
683 });
684 }
685 }
686
687 result
688 }
689
690 pub fn add_section(&mut self, section: Section<'a>) -> usize {
700 let index = self.sections.len();
701 self.change_tracker.record(Change::SectionAdded {
702 section: section.clone(),
703 index,
704 });
705 self.sections.push(section);
706 index
707 }
708
709 pub fn remove_section(
723 &mut self,
724 index: usize,
725 ) -> core::result::Result<Section<'a>, ParseError> {
726 if index < self.sections.len() {
727 let section = self.sections.remove(index);
728 self.change_tracker.record(Change::SectionRemoved {
729 section_type: section.section_type(),
730 index,
731 });
732 Ok(section)
733 } else {
734 Err(ParseError::IndexOutOfBounds)
735 }
736 }
737
738 pub fn add_style(&mut self, style: Style<'a>) -> usize {
750 let styles_section_index = self
752 .sections
753 .iter()
754 .position(|s| matches!(s, Section::Styles(_)));
755
756 if let Some(index) = styles_section_index {
757 if let Section::Styles(styles) = &mut self.sections[index] {
758 styles.push(style);
759 styles.len() - 1
760 } else {
761 unreachable!("Section type mismatch");
762 }
763 } else {
764 self.sections.push(Section::Styles(vec![style]));
766 0
767 }
768 }
769
770 pub fn add_event(&mut self, event: Event<'a>) -> usize {
782 let events_section_index = self
784 .sections
785 .iter()
786 .position(|s| matches!(s, Section::Events(_)));
787
788 if let Some(index) = events_section_index {
789 if let Section::Events(events) = &mut self.sections[index] {
790 events.push(event);
791 events.len() - 1
792 } else {
793 unreachable!("Section type mismatch");
794 }
795 } else {
796 self.sections.push(Section::Events(vec![event]));
798 0
799 }
800 }
801
802 pub fn set_styles_format(&mut self, format: Vec<&'a str>) {
804 self.styles_format = Some(format);
805 }
806
807 pub fn set_events_format(&mut self, format: Vec<&'a str>) {
809 self.events_format = Some(format);
810 }
811
812 pub fn batch_update_lines(
825 &mut self,
826 operations: Vec<UpdateOperation<'a>>,
827 ) -> BatchUpdateResult<'a> {
828 let mut result = BatchUpdateResult {
829 updated: Vec::with_capacity(operations.len()),
830 failed: Vec::new(),
831 };
832
833 let mut sorted_ops = operations;
835 sorted_ops.sort_by_key(|op| op.offset);
836
837 for op in sorted_ops {
838 match self.update_line_at_offset(op.offset, op.new_line, op.line_number) {
839 Ok(old_content) => {
840 result.updated.push((op.offset, old_content));
841 }
842 Err(e) => {
843 result.failed.push((op.offset, e));
844 }
845 }
846 }
847
848 result
849 }
850
851 pub fn batch_add_styles(&mut self, batch: StyleBatch<'a>) -> Vec<usize> {
863 let mut indices = Vec::with_capacity(batch.styles.len());
864
865 let styles_section_index = self
867 .sections
868 .iter()
869 .position(|s| matches!(s, Section::Styles(_)));
870
871 if let Some(index) = styles_section_index {
872 if let Section::Styles(styles) = &mut self.sections[index] {
873 let start_index = styles.len();
874 styles.extend(batch.styles);
875 indices.extend(start_index..styles.len());
876 }
877 } else {
878 let count = batch.styles.len();
880 self.sections.push(Section::Styles(batch.styles));
881 indices.extend(0..count);
882 }
883
884 indices
885 }
886
887 pub fn batch_add_events(&mut self, batch: EventBatch<'a>) -> Vec<usize> {
899 let mut indices = Vec::with_capacity(batch.events.len());
900
901 let events_section_index = self
903 .sections
904 .iter()
905 .position(|s| matches!(s, Section::Events(_)));
906
907 if let Some(index) = events_section_index {
908 if let Section::Events(events) = &mut self.sections[index] {
909 let start_index = events.len();
910 events.extend(batch.events);
911 indices.extend(start_index..events.len());
912 }
913 } else {
914 let count = batch.events.len();
916 self.sections.push(Section::Events(batch.events));
917 indices.extend(0..count);
918 }
919
920 indices
921 }
922
923 pub fn atomic_batch_update(
942 &mut self,
943 updates: Vec<UpdateOperation<'a>>,
944 style_additions: Option<StyleBatch<'a>>,
945 event_additions: Option<EventBatch<'a>>,
946 ) -> core::result::Result<(), ParseError> {
947 for op in &updates {
949 let section_found = self.sections.iter().any(|s| {
951 s.span()
952 .is_some_and(|span| span.start <= op.offset && op.offset < span.end)
953 });
954 if !section_found {
955 return Err(ParseError::SectionNotFound);
956 }
957
958 self.parse_line_auto(op.new_line, op.line_number)?;
960 }
961
962 let mut temp_script = self.clone();
965
966 for op in updates {
968 temp_script.update_line_at_offset(op.offset, op.new_line, op.line_number)?;
969 }
970
971 if let Some(styles) = style_additions {
973 temp_script.batch_add_styles(styles);
974 }
975
976 if let Some(events) = event_additions {
978 temp_script.batch_add_events(events);
979 }
980
981 *self = temp_script;
983 Ok(())
984 }
985
986 pub fn enable_change_tracking(&mut self) {
991 self.change_tracker.enable();
992 }
993
994 pub fn disable_change_tracking(&mut self) {
998 self.change_tracker.disable();
999 }
1000
1001 #[must_use]
1003 pub const fn is_change_tracking_enabled(&self) -> bool {
1004 self.change_tracker.is_enabled()
1005 }
1006
1007 #[must_use]
1012 pub fn changes(&self) -> &[Change<'a>] {
1013 self.change_tracker.changes()
1014 }
1015
1016 pub fn clear_changes(&mut self) {
1020 self.change_tracker.clear();
1021 }
1022
1023 #[must_use]
1025 pub fn change_count(&self) -> usize {
1026 self.change_tracker.len()
1027 }
1028
1029 #[must_use]
1042 pub fn diff(&self, other: &Self) -> Vec<Change<'a>> {
1043 let mut changes = Vec::new();
1044
1045 let max_sections = self.sections.len().max(other.sections.len());
1047
1048 for i in 0..max_sections {
1049 match (self.sections.get(i), other.sections.get(i)) {
1050 (Some(self_section), Some(other_section)) => {
1051 if self_section != other_section {
1053 changes.push(Change::SectionRemoved {
1056 section_type: other_section.section_type(),
1057 index: i,
1058 });
1059 changes.push(Change::SectionAdded {
1060 section: self_section.clone(),
1061 index: i,
1062 });
1063 }
1064 }
1065 (Some(self_section), None) => {
1066 changes.push(Change::SectionAdded {
1068 section: self_section.clone(),
1069 index: i,
1070 });
1071 }
1072 (None, Some(other_section)) => {
1073 changes.push(Change::SectionRemoved {
1075 section_type: other_section.section_type(),
1076 index: i,
1077 });
1078 }
1079 (None, None) => {
1080 unreachable!("max_sections calculation error");
1082 }
1083 }
1084 }
1085
1086 changes
1087 }
1088
1089 #[must_use]
1101 pub fn affected_sections(
1102 &self,
1103 change: &crate::parser::incremental::TextChange,
1104 ) -> Vec<SectionType> {
1105 self.sections
1106 .iter()
1107 .filter(|section| {
1108 section.span().is_some_and(|span| {
1109 let section_range = span.start..span.end;
1110
1111 let overlaps = change.range.start < section_range.end
1113 && change.range.end > section_range.start;
1114
1115 let inserts_at_end =
1118 change.range.is_empty() && change.range.start == section_range.end;
1119
1120 overlaps || inserts_at_end
1121 })
1122 })
1123 .map(Section::section_type)
1124 .collect()
1125 }
1126
1127 pub fn parse_line_in_section(
1146 &self,
1147 section_type: SectionType,
1148 line: &'a str,
1149 line_number: u32,
1150 ) -> Result<LineContent<'a>> {
1151 match section_type {
1152 SectionType::Events => {
1153 let format = self
1154 .events_format()
1155 .ok_or(crate::utils::errors::CoreError::Parse(
1156 ParseError::MissingFormat,
1157 ))?;
1158 crate::parser::sections::EventsParser::parse_event_line(line, format, line_number)
1159 .map(|event| LineContent::Event(Box::new(event)))
1160 .map_err(crate::utils::errors::CoreError::Parse)
1161 }
1162 SectionType::Styles => {
1163 let format = self
1164 .styles_format()
1165 .ok_or(crate::utils::errors::CoreError::Parse(
1166 ParseError::MissingFormat,
1167 ))?;
1168 crate::parser::sections::StylesParser::parse_style_line(line, format, line_number)
1169 .map(|style| LineContent::Style(Box::new(style)))
1170 .map_err(crate::utils::errors::CoreError::Parse)
1171 }
1172 SectionType::ScriptInfo => {
1173 if let Some((key, value)) = line.split_once(':') {
1175 Ok(LineContent::Field(key.trim(), value.trim()))
1176 } else {
1177 Err(crate::utils::errors::CoreError::Parse(
1178 ParseError::InvalidFieldFormat {
1179 line: line_number as usize,
1180 },
1181 ))
1182 }
1183 }
1184 _ => Err(crate::utils::errors::CoreError::Parse(
1185 ParseError::UnsupportedSection(section_type),
1186 )),
1187 }
1188 }
1189
1190 pub fn parse_incremental(
1208 &self,
1209 new_source: &'a str,
1210 change: &crate::parser::incremental::TextChange,
1211 ) -> Result<Self> {
1212 use crate::parser::sections::SectionFormats;
1213
1214 let affected_sections = self.affected_sections(change);
1216
1217 if affected_sections.is_empty() {
1218 return Ok(Script::from_parts(
1220 new_source,
1221 self.version(),
1222 self.sections.clone(),
1223 vec![], self.styles_format.clone(),
1225 self.events_format.clone(),
1226 ));
1227 }
1228
1229 let formats = SectionFormats {
1231 styles_format: self.styles_format().map(<[&str]>::to_vec),
1232 events_format: self.events_format().map(<[&str]>::to_vec),
1233 };
1234
1235 let mut new_sections = Vec::with_capacity(self.sections.len());
1237
1238 let section_headers = [
1241 ("[Script Info]", SectionType::ScriptInfo),
1242 ("[V4+ Styles]", SectionType::Styles),
1243 ("[Events]", SectionType::Events),
1244 ("[Fonts]", SectionType::Fonts),
1245 ("[Graphics]", SectionType::Graphics),
1246 ];
1247
1248 for (idx, section) in self.sections.iter().enumerate() {
1250 let section_type = section.section_type();
1251
1252 if affected_sections.contains(§ion_type) {
1253 let header_str = section_headers
1255 .iter()
1256 .find(|(_, t)| *t == section_type)
1257 .map_or("[Unknown]", |(h, _)| *h);
1258
1259 if let Some(header_pos) = new_source.find(header_str) {
1261 let section_end = if idx + 1 < self.sections.len() {
1263 let next_section_type = self.sections[idx + 1].section_type();
1265 let next_header = section_headers
1266 .iter()
1267 .find(|(_, t)| *t == next_section_type)
1268 .map_or("[Unknown]", |(h, _)| *h);
1269
1270 new_source[header_pos + header_str.len()..]
1271 .find(next_header)
1272 .map_or(new_source.len(), |pos| header_pos + header_str.len() + pos)
1273 } else {
1274 new_source.len()
1275 };
1276
1277 let section_text = &new_source[header_pos..section_end];
1279
1280 let parser = Parser::new(section_text);
1282 let parsed_script = parser.parse();
1283
1284 if let Some(parsed_section) = parsed_script
1287 .sections
1288 .into_iter()
1289 .find(|s| s.section_type() == section_type)
1290 {
1291 new_sections.push(parsed_section);
1292 }
1293 }
1294 } else {
1295 let section_span = section.span();
1297 if let Some(span) = section_span {
1298 if change.range.end <= span.start {
1299 new_sections.push(Self::adjust_section_spans(section, change));
1301 } else {
1302 new_sections.push(section.clone());
1304 }
1305 } else {
1306 new_sections.push(section.clone());
1307 }
1308 }
1309 }
1310
1311 Ok(Script::from_parts(
1313 new_source,
1314 self.version(),
1315 new_sections,
1316 vec![], formats.styles_format.clone(),
1318 formats.events_format.clone(),
1319 ))
1320 }
1321
1322 fn adjust_section_spans(
1324 section: &Section<'a>,
1325 change: &crate::parser::incremental::TextChange,
1326 ) -> Section<'a> {
1327 use crate::parser::ast::Span;
1328
1329 let new_len = change.new_text.len();
1331 let old_len = change.range.end - change.range.start;
1332
1333 let adjust_span = |span: &Span| -> Span {
1335 let new_start = if new_len >= old_len {
1336 span.start + (new_len - old_len)
1337 } else {
1338 span.start.saturating_sub(old_len - new_len)
1339 };
1340
1341 let new_end = if new_len >= old_len {
1342 span.end + (new_len - old_len)
1343 } else {
1344 span.end.saturating_sub(old_len - new_len)
1345 };
1346
1347 Span::new(new_start, new_end, span.line, span.column)
1348 };
1349
1350 match section {
1352 Section::ScriptInfo(info) => {
1353 let mut new_info = info.clone();
1354 new_info.span = adjust_span(&info.span);
1355 Section::ScriptInfo(new_info)
1356 }
1357 Section::Styles(styles) => {
1358 let new_styles: Vec<_> = styles
1359 .iter()
1360 .map(|style| {
1361 let mut new_style = style.clone();
1362 new_style.span = adjust_span(&style.span);
1363 new_style
1364 })
1365 .collect();
1366 Section::Styles(new_styles)
1367 }
1368 Section::Events(events) => {
1369 let new_events: Vec<_> = events
1370 .iter()
1371 .map(|event| {
1372 let mut new_event = event.clone();
1373 new_event.span = adjust_span(&event.span);
1374 new_event
1375 })
1376 .collect();
1377 Section::Events(new_events)
1378 }
1379 Section::Fonts(fonts) => {
1380 let new_fonts: Vec<_> = fonts
1381 .iter()
1382 .map(|font| {
1383 let mut new_font = font.clone();
1384 new_font.span = adjust_span(&font.span);
1385 new_font
1386 })
1387 .collect();
1388 Section::Fonts(new_fonts)
1389 }
1390 Section::Graphics(graphics) => {
1391 let new_graphics: Vec<_> = graphics
1392 .iter()
1393 .map(|graphic| {
1394 let mut new_graphic = graphic.clone();
1395 new_graphic.span = adjust_span(&graphic.span);
1396 new_graphic
1397 })
1398 .collect();
1399 Section::Graphics(new_graphics)
1400 }
1401 }
1402 }
1403
1404 #[must_use]
1419 pub fn to_ass_string(&self) -> alloc::string::String {
1420 let mut result = String::new();
1421
1422 for (idx, section) in self.sections.iter().enumerate() {
1423 if idx > 0 {
1425 result.push('\n');
1426 }
1427
1428 match section {
1429 Section::ScriptInfo(info) => {
1430 result.push_str(&info.to_ass_string());
1431 }
1432 Section::Styles(styles) => {
1433 result.push_str("[V4+ Styles]\n");
1434
1435 if let Some(format) = &self.styles_format {
1437 result.push_str("Format: ");
1438 result.push_str(&format.join(", "));
1439 result.push('\n');
1440 }
1441
1442 for style in styles {
1444 if let Some(format) = &self.styles_format {
1445 result.push_str(&style.to_ass_string_with_format(format));
1446 } else {
1447 result.push_str(&style.to_ass_string());
1448 }
1449 result.push('\n');
1450 }
1451 }
1452 Section::Events(events) => {
1453 result.push_str("[Events]\n");
1454
1455 if let Some(format) = &self.events_format {
1457 result.push_str("Format: ");
1458 result.push_str(&format.join(", "));
1459 result.push('\n');
1460 }
1461
1462 for event in events {
1464 if let Some(format) = &self.events_format {
1465 result.push_str(&event.to_ass_string_with_format(format));
1466 } else {
1467 result.push_str(&event.to_ass_string());
1468 }
1469 result.push('\n');
1470 }
1471 }
1472 Section::Fonts(fonts) => {
1473 result.push_str("[Fonts]\n");
1474 for font in fonts {
1475 result.push_str(&font.to_ass_string());
1476 }
1477 }
1478 Section::Graphics(graphics) => {
1479 result.push_str("[Graphics]\n");
1480 for graphic in graphics {
1481 result.push_str(&graphic.to_ass_string());
1482 }
1483 }
1484 }
1485 }
1486
1487 result
1488 }
1489}
1490
1491#[cfg(feature = "stream")]
1493#[derive(Debug, Clone)]
1494pub struct ScriptDelta<'a> {
1495 pub added: Vec<Section<'a>>,
1497
1498 pub modified: Vec<(usize, Section<'a>)>,
1500
1501 pub removed: Vec<usize>,
1503
1504 pub new_issues: Vec<ParseIssue>,
1506}
1507
1508#[cfg(feature = "stream")]
1513fn sections_equal_ignoring_spans(old: &Section<'_>, new: &Section<'_>) -> bool {
1514 use Section::{Events, Fonts, Graphics, ScriptInfo, Styles};
1515
1516 match (old, new) {
1517 (ScriptInfo(old_info), ScriptInfo(new_info)) => {
1518 old_info.fields == new_info.fields
1520 }
1521 (Styles(old_styles), Styles(new_styles)) => {
1522 if old_styles.len() != new_styles.len() {
1524 return false;
1525 }
1526
1527 for (old_style, new_style) in old_styles.iter().zip(new_styles.iter()) {
1528 if !styles_equal_ignoring_span(old_style, new_style) {
1529 return false;
1530 }
1531 }
1532 true
1533 }
1534 (Events(old_events), Events(new_events)) => {
1535 if old_events.len() != new_events.len() {
1537 return false;
1538 }
1539
1540 for (old_event, new_event) in old_events.iter().zip(new_events.iter()) {
1541 if !events_equal_ignoring_span(old_event, new_event) {
1542 return false;
1543 }
1544 }
1545 true
1546 }
1547 (Fonts(old_fonts), Fonts(new_fonts)) => {
1548 if old_fonts.len() != new_fonts.len() {
1550 return false;
1551 }
1552
1553 for (old_font, new_font) in old_fonts.iter().zip(new_fonts.iter()) {
1554 if !fonts_equal_ignoring_span(old_font, new_font) {
1555 return false;
1556 }
1557 }
1558 true
1559 }
1560 (Graphics(old_graphics), Graphics(new_graphics)) => {
1561 if old_graphics.len() != new_graphics.len() {
1563 return false;
1564 }
1565
1566 for (old_graphic, new_graphic) in old_graphics.iter().zip(new_graphics.iter()) {
1567 if !graphics_equal_ignoring_span(old_graphic, new_graphic) {
1568 return false;
1569 }
1570 }
1571 true
1572 }
1573 _ => false, }
1575}
1576
1577#[cfg(feature = "stream")]
1579fn styles_equal_ignoring_span(old: &Style<'_>, new: &Style<'_>) -> bool {
1580 old.name == new.name
1581 && old.parent == new.parent
1582 && old.fontname == new.fontname
1583 && old.fontsize == new.fontsize
1584 && old.primary_colour == new.primary_colour
1585 && old.secondary_colour == new.secondary_colour
1586 && old.outline_colour == new.outline_colour
1587 && old.back_colour == new.back_colour
1588 && old.bold == new.bold
1589 && old.italic == new.italic
1590 && old.underline == new.underline
1591 && old.strikeout == new.strikeout
1592 && old.scale_x == new.scale_x
1593 && old.scale_y == new.scale_y
1594 && old.spacing == new.spacing
1595 && old.angle == new.angle
1596 && old.border_style == new.border_style
1597 && old.outline == new.outline
1598 && old.shadow == new.shadow
1599 && old.alignment == new.alignment
1600 && old.margin_l == new.margin_l
1601 && old.margin_r == new.margin_r
1602 && old.margin_v == new.margin_v
1603 && old.margin_t == new.margin_t
1604 && old.margin_b == new.margin_b
1605 && old.encoding == new.encoding
1606 && old.relative_to == new.relative_to
1607 }
1609
1610#[cfg(feature = "stream")]
1612fn events_equal_ignoring_span(old: &Event<'_>, new: &Event<'_>) -> bool {
1613 old.event_type == new.event_type
1614 && old.layer == new.layer
1615 && old.start == new.start
1616 && old.end == new.end
1617 && old.style == new.style
1618 && old.name == new.name
1619 && old.margin_l == new.margin_l
1620 && old.margin_r == new.margin_r
1621 && old.margin_v == new.margin_v
1622 && old.margin_t == new.margin_t
1623 && old.margin_b == new.margin_b
1624 && old.effect == new.effect
1625 && old.text == new.text
1626 }
1628
1629#[cfg(feature = "stream")]
1631fn fonts_equal_ignoring_span(old: &Font<'_>, new: &Font<'_>) -> bool {
1632 old.filename == new.filename && old.data_lines == new.data_lines
1633 }
1635
1636#[cfg(feature = "stream")]
1638fn graphics_equal_ignoring_span(old: &Graphic<'_>, new: &Graphic<'_>) -> bool {
1639 old.filename == new.filename && old.data_lines == new.data_lines
1640 }
1642
1643#[cfg(feature = "stream")]
1658#[must_use]
1659pub fn calculate_delta<'a>(old_script: &Script<'a>, new_script: &Script<'a>) -> ScriptDelta<'a> {
1660 let mut added = Vec::new();
1661 let mut modified = Vec::new();
1662 let mut removed = Vec::new();
1663
1664 let old_sections: Vec<_> = old_script.sections().iter().collect();
1666 let new_sections: Vec<_> = new_script.sections().iter().collect();
1667
1668 for (idx, old_section) in old_sections.iter().enumerate() {
1670 let old_type = old_section.section_type();
1671
1672 if let Some((_new_idx, new_section)) = new_sections
1674 .iter()
1675 .enumerate()
1676 .find(|(_, s)| s.section_type() == old_type)
1677 {
1678 if !sections_equal_ignoring_spans(old_section, new_section) {
1680 modified.push((idx, (*new_section).clone()));
1681 }
1682 } else {
1683 removed.push(idx);
1685 }
1686 }
1687
1688 for new_section in &new_sections {
1690 let new_type = new_section.section_type();
1691
1692 if !old_sections.iter().any(|s| s.section_type() == new_type) {
1694 added.push((*new_section).clone());
1695 }
1696 }
1697
1698 let new_issues: Vec<_> = new_script.issues().to_vec();
1702
1703 ScriptDelta {
1704 added,
1705 modified,
1706 removed,
1707 new_issues,
1708 }
1709}
1710
1711#[cfg(feature = "stream")]
1713#[derive(Debug, Clone)]
1714pub struct ScriptDeltaOwned {
1715 pub added: Vec<String>,
1717
1718 pub modified: Vec<(usize, String)>,
1720
1721 pub removed: Vec<usize>,
1723
1724 pub new_issues: Vec<ParseIssue>,
1726}
1727
1728#[cfg(feature = "stream")]
1729impl ScriptDelta<'_> {
1730 #[must_use]
1732 pub fn is_empty(&self) -> bool {
1733 self.added.is_empty()
1734 && self.modified.is_empty()
1735 && self.removed.is_empty()
1736 && self.new_issues.is_empty()
1737 }
1738}
1739
1740#[derive(Debug)]
1745pub struct ScriptBuilder<'a> {
1746 #[cfg(feature = "plugins")]
1748 registry: Option<&'a ExtensionRegistry>,
1749}
1750
1751impl<'a> ScriptBuilder<'a> {
1752 #[must_use]
1754 pub const fn new() -> Self {
1755 Self {
1756 #[cfg(feature = "plugins")]
1757 registry: None,
1758 }
1759 }
1760
1761 #[cfg(feature = "plugins")]
1766 #[must_use]
1767 pub const fn with_registry(mut self, registry: &'a ExtensionRegistry) -> Self {
1768 self.registry = Some(registry);
1769 self
1770 }
1771
1772 pub fn parse(self, source: &'a str) -> Result<Script<'a>> {
1783 #[cfg(feature = "plugins")]
1784 let parser = Parser::new_with_registry(source, self.registry);
1785 #[cfg(not(feature = "plugins"))]
1786 let parser = Parser::new(source);
1787
1788 Ok(parser.parse())
1789 }
1790}
1791
1792impl Default for ScriptBuilder<'_> {
1793 fn default() -> Self {
1794 Self::new()
1795 }
1796}
1797
1798#[cfg(test)]
1799mod tests {
1800 use super::*;
1801 use crate::parser::ast::SectionType;
1802 #[cfg(not(feature = "std"))]
1803 use alloc::{format, string::String, vec};
1804
1805 #[test]
1806 fn parse_minimal_script() {
1807 let script = Script::parse("[Script Info]\nTitle: Test").unwrap();
1808 assert_eq!(script.sections().len(), 1);
1809 assert_eq!(script.version(), ScriptVersion::AssV4);
1810 }
1811
1812 #[test]
1813 fn parse_with_script_type() {
1814 let script = Script::parse("[Script Info]\nScriptType: v4.00+\nTitle: Test").unwrap();
1815 assert_eq!(script.version(), ScriptVersion::AssV4);
1816 }
1817
1818 #[test]
1819 fn parse_with_bom() {
1820 let script = Script::parse("\u{FEFF}[Script Info]\nTitle: Test").unwrap();
1821 assert_eq!(script.sections().len(), 1);
1822 }
1823
1824 #[test]
1825 fn parse_empty_input() {
1826 let script = Script::parse("").unwrap();
1827 assert_eq!(script.sections().len(), 0);
1828 }
1829
1830 #[test]
1831 fn parse_multiple_sections() {
1832 let content = "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name, Fontname\nStyle: Default,Arial\n\n[Events]\nFormat: Layer, Start, End, Style, Text\nDialogue: 0,0:00:00.00,0:00:05.00,Default,Hello World";
1833 let script = Script::parse(content).unwrap();
1834 assert_eq!(script.sections().len(), 3);
1835 assert_eq!(script.version(), ScriptVersion::AssV4);
1836 }
1837
1838 #[test]
1839 fn script_version_detection() {
1840 let script = Script::parse("[Script Info]\nTitle: Test").unwrap();
1841 assert_eq!(script.version(), ScriptVersion::AssV4);
1842 }
1843
1844 #[test]
1845 fn script_source_access() {
1846 let content = "[Script Info]\nTitle: Test";
1847 let script = Script::parse(content).unwrap();
1848 assert_eq!(script.source(), content);
1849 }
1850
1851 #[test]
1852 fn script_sections_access() {
1853 let content = "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name";
1854 let script = Script::parse(content).unwrap();
1855 let sections = script.sections();
1856 assert_eq!(sections.len(), 2);
1857 }
1858
1859 #[test]
1860 fn script_issues_access() {
1861 let script = Script::parse("[Script Info]\nTitle: Test").unwrap();
1862 let issues = script.issues();
1863 assert!(
1865 issues.is_empty()
1866 || issues
1867 .iter()
1868 .all(|i| matches!(i.severity, crate::parser::errors::IssueSeverity::Warning))
1869 );
1870 }
1871
1872 #[test]
1873 fn find_section_by_type() {
1874 let content =
1875 "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name\n\n[Events]\nFormat: Layer";
1876 let script = Script::parse(content).unwrap();
1877
1878 let script_info = script.find_section(SectionType::ScriptInfo);
1879 assert!(script_info.is_some());
1880
1881 let styles = script.find_section(SectionType::Styles);
1882 assert!(styles.is_some());
1883
1884 let events = script.find_section(SectionType::Events);
1885 assert!(events.is_some());
1886 }
1887
1888 #[test]
1889 fn find_section_missing() {
1890 let content = "[Script Info]\nTitle: Test";
1891 let script = Script::parse(content).unwrap();
1892
1893 let styles = script.find_section(SectionType::Styles);
1894 assert!(styles.is_none());
1895
1896 let events = script.find_section(SectionType::Events);
1897 assert!(events.is_none());
1898 }
1899
1900 #[test]
1901 fn script_clone() {
1902 let content = "[Script Info]\nTitle: Test";
1903 let script = Script::parse(content).unwrap();
1904 let cloned = script.clone();
1905
1906 assert_eq!(script, cloned);
1907 assert_eq!(script.source(), cloned.source());
1908 assert_eq!(script.version(), cloned.version());
1909 assert_eq!(script.sections().len(), cloned.sections().len());
1910 }
1911
1912 #[test]
1913 fn script_debug() {
1914 let script = Script::parse("[Script Info]\nTitle: Test").unwrap();
1915 let debug_str = format!("{script:?}");
1916 assert!(debug_str.contains("Script"));
1917 }
1918
1919 #[test]
1920 fn script_equality() {
1921 let content = "[Script Info]\nTitle: Test";
1922 let script1 = Script::parse(content).unwrap();
1923 let script2 = Script::parse(content).unwrap();
1924 assert_eq!(script1, script2);
1925
1926 let different_content = "[Script Info]\nTitle: Different";
1927 let script3 = Script::parse(different_content).unwrap();
1928 assert_ne!(script1, script3);
1929 }
1930
1931 #[test]
1932 fn parse_whitespace_only() {
1933 let script = Script::parse(" \n\n \t \n").unwrap();
1934 assert_eq!(script.sections().len(), 0);
1935 }
1936
1937 #[test]
1938 fn parse_comments_only() {
1939 let script = Script::parse("!: This is a comment\n; Another comment").unwrap();
1940 assert_eq!(script.sections().len(), 0);
1941 }
1942
1943 #[test]
1944 fn parse_multiple_script_info_sections() {
1945 let content = "[Script Info]\nTitle: First\n\n[Script Info]\nTitle: Second";
1946 let script = Script::parse(content).unwrap();
1947 assert!(!script.sections().is_empty());
1949 }
1950
1951 #[test]
1952 fn parse_case_insensitive_sections() {
1953 let content = "[script info]\nTitle: Test\n\n[v4+ styles]\nFormat: Name";
1954 let _script = Script::parse(content).unwrap();
1955 }
1958
1959 #[test]
1960 fn parse_malformed_but_recoverable() {
1961 let content = "[Script Info]\nTitle: Test\nMalformed line without colon\nAuthor: Someone";
1962 let script = Script::parse(content).unwrap();
1963 assert_eq!(script.sections().len(), 1);
1964 let issues = script.issues();
1966 assert!(issues.is_empty() || !issues.is_empty()); }
1968
1969 #[test]
1970 fn parse_with_various_line_endings() {
1971 let content_crlf = "[Script Info]\r\nTitle: Test\r\n";
1972 let script_crlf = Script::parse(content_crlf).unwrap();
1973 assert_eq!(script_crlf.sections().len(), 1);
1974
1975 let content_lf = "[Script Info]\nTitle: Test\n";
1976 let script_lf = Script::parse(content_lf).unwrap();
1977 assert_eq!(script_lf.sections().len(), 1);
1978 }
1979
1980 #[test]
1981 fn from_parts_constructor() {
1982 let source = "[Script Info]\nTitle: Test";
1983 let sections = Vec::new();
1984 let issues = Vec::new();
1985
1986 let script = Script::from_parts(source, ScriptVersion::AssV4, sections, issues, None, None);
1987 assert_eq!(script.source(), source);
1988 assert_eq!(script.version(), ScriptVersion::AssV4);
1989 assert_eq!(script.sections().len(), 0);
1990 assert_eq!(script.issues().len(), 0);
1991 }
1992
1993 #[cfg(debug_assertions)]
1994 #[test]
1995 fn validate_spans() {
1996 let script = Script::parse("[Script Info]\nTitle: Test").unwrap();
1997 assert!(script.validate_spans() || !script.validate_spans()); }
2001
2002 #[test]
2003 fn parse_unicode_content() {
2004 let content = "[Script Info]\nTitle: Unicode Test 测试 🎬\nAuthor: アニメ";
2005 let script = Script::parse(content).unwrap();
2006 assert_eq!(script.sections().len(), 1);
2007 assert_eq!(script.source(), content);
2008 }
2009
2010 #[test]
2011 fn parse_very_long_content() {
2012 #[cfg(not(feature = "std"))]
2013 use alloc::fmt::Write;
2014 #[cfg(feature = "std")]
2015 use std::fmt::Write;
2016
2017 let mut content = String::from("[Script Info]\nTitle: Long Test\n");
2018 for i in 0..1000 {
2019 writeln!(
2020 content,
2021 "Comment{i}: This is a very long comment line to test performance"
2022 )
2023 .unwrap();
2024 }
2025
2026 let script = Script::parse(&content).unwrap();
2027 assert_eq!(script.sections().len(), 1);
2028 }
2029
2030 #[test]
2031 fn parse_nested_brackets() {
2032 let content = "[Script Info]\nTitle: Test [with] brackets\nComment: [nested [brackets]]";
2033 let script = Script::parse(content).unwrap();
2034 assert_eq!(script.sections().len(), 1);
2035 }
2036
2037 #[cfg(feature = "stream")]
2038 #[test]
2039 fn script_delta_is_empty() {
2040 use crate::parser::ast::Span;
2041
2042 let delta = ScriptDelta {
2043 added: Vec::new(),
2044 modified: Vec::new(),
2045 removed: Vec::new(),
2046 new_issues: Vec::new(),
2047 };
2048 assert!(delta.is_empty());
2049
2050 let non_empty_delta = ScriptDelta {
2051 added: vec![],
2052 modified: vec![(
2053 0,
2054 Section::ScriptInfo(crate::parser::ast::ScriptInfo {
2055 fields: Vec::new(),
2056 span: Span::new(0, 0, 0, 0),
2057 }),
2058 )],
2059 removed: Vec::new(),
2060 new_issues: Vec::new(),
2061 };
2062 assert!(!non_empty_delta.is_empty());
2063 }
2064
2065 #[cfg(feature = "stream")]
2066 #[test]
2067 fn script_delta_debug() {
2068 let delta = ScriptDelta {
2069 added: Vec::new(),
2070 modified: Vec::new(),
2071 removed: Vec::new(),
2072 new_issues: Vec::new(),
2073 };
2074 let debug_str = format!("{delta:?}");
2075 assert!(debug_str.contains("ScriptDelta"));
2076 }
2077
2078 #[cfg(feature = "stream")]
2079 #[test]
2080 fn script_delta_owned_debug() {
2081 let delta = ScriptDeltaOwned {
2082 added: Vec::new(),
2083 modified: Vec::new(),
2084 removed: Vec::new(),
2085 new_issues: Vec::new(),
2086 };
2087 let debug_str = format!("{delta:?}");
2088 assert!(debug_str.contains("ScriptDeltaOwned"));
2089 }
2090
2091 #[cfg(feature = "stream")]
2092 #[test]
2093 fn parse_partial_basic() {
2094 let content = "[Script Info]\nTitle: Original";
2095 let script = Script::parse(content).unwrap();
2096
2097 let result = script.parse_partial(0..content.len(), "[Script Info]\nTitle: Modified");
2099 assert!(result.is_ok() || result.is_err());
2101 }
2102
2103 #[test]
2104 fn parse_empty_sections() {
2105 let content = "[Script Info]\n\n[V4+ Styles]\n\n[Events]\n";
2106 let script = Script::parse(content).unwrap();
2107 assert_eq!(script.sections().len(), 3);
2108 }
2109
2110 #[test]
2111 fn parse_section_with_only_format() {
2112 let content = "[V4+ Styles]\nFormat: Name, Fontname, Fontsize";
2113 let script = Script::parse(content).unwrap();
2114 assert_eq!(script.sections().len(), 1);
2115 }
2116
2117 #[test]
2118 fn parse_events_with_complex_text() {
2119 let content = r"[Events]
2120Format: Layer, Start, End, Style, Text
2121Dialogue: 0,0:00:00.00,0:00:05.00,Default,{\b1}Bold text{\b0} and {\i1}italic{\i0}
2122Comment: 0,0:00:05.00,0:00:10.00,Default,This is a comment
2123";
2124 let script = Script::parse(content).unwrap();
2125 assert_eq!(script.sections().len(), 1);
2126 }
2127
2128 #[cfg(debug_assertions)]
2129 #[test]
2130 fn validate_spans_comprehensive() {
2131 let content = "[Script Info]\nTitle: Test\nAuthor: Someone";
2132 let script = Script::parse(content).unwrap();
2133
2134 assert!(script.validate_spans());
2136
2137 assert_eq!(script.source(), content);
2139 }
2140
2141 #[test]
2142 fn script_accessor_methods() {
2143 let content = "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name";
2144 let script = Script::parse(content).unwrap();
2145
2146 assert_eq!(script.version(), ScriptVersion::AssV4);
2148 assert_eq!(script.sections().len(), 2);
2149 assert_eq!(script.source(), content);
2150 let _ = script.issues();
2152
2153 assert!(script.find_section(SectionType::ScriptInfo).is_some());
2155 assert!(script.find_section(SectionType::Styles).is_some());
2156 assert!(script.find_section(SectionType::Events).is_none());
2157 }
2158
2159 #[test]
2160 fn from_parts_comprehensive() {
2161 use crate::parser::ast::{ScriptInfo, Section, Span};
2162
2163 let source = "[Script Info]\nTitle: Custom";
2164 let mut sections = Vec::new();
2165 let issues = Vec::new();
2166
2167 let script1 = Script::from_parts(
2169 source,
2170 ScriptVersion::AssV4,
2171 sections.clone(),
2172 issues.clone(),
2173 None,
2174 None,
2175 );
2176 assert_eq!(script1.source(), source);
2177 assert_eq!(script1.version(), ScriptVersion::AssV4);
2178 assert_eq!(script1.sections().len(), 0);
2179 assert_eq!(script1.issues().len(), 0);
2180
2181 let script_info = ScriptInfo {
2183 fields: Vec::new(),
2184 span: Span::new(0, 0, 0, 0),
2185 };
2186 sections.push(Section::ScriptInfo(script_info));
2187
2188 let script2 =
2189 Script::from_parts(source, ScriptVersion::AssV4, sections, issues, None, None);
2190 assert_eq!(script2.sections().len(), 1);
2191 }
2192
2193 #[test]
2194 fn format_preservation() {
2195 let content = "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, Bold\nStyle: Default,Arial,20,1\n\n[Events]\nFormat: Layer, Start, End, Style, Text\nDialogue: 0,0:00:00.00,0:00:05.00,Default,Hello";
2196
2197 let script = Script::parse(content).unwrap();
2198
2199 let styles_format = script.styles_format().unwrap();
2201 assert_eq!(styles_format.len(), 4);
2202 assert_eq!(styles_format[0], "Name");
2203 assert_eq!(styles_format[1], "Fontname");
2204 assert_eq!(styles_format[2], "Fontsize");
2205 assert_eq!(styles_format[3], "Bold");
2206
2207 let events_format = script.events_format().unwrap();
2208 assert_eq!(events_format.len(), 5);
2209 assert_eq!(events_format[0], "Layer");
2210 assert_eq!(events_format[1], "Start");
2211 assert_eq!(events_format[2], "End");
2212 assert_eq!(events_format[3], "Style");
2213 assert_eq!(events_format[4], "Text");
2214 }
2215
2216 #[test]
2217 fn context_aware_style_parsing() {
2218 let content = "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name, Fontname, Bold\nStyle: Default,Arial,1";
2219 let script = Script::parse(content).unwrap();
2220
2221 let style_line = "NewStyle,Times,0";
2223 let result = script.parse_style_line_with_context(style_line, 10);
2224 assert!(result.is_ok());
2225
2226 let style = result.unwrap();
2227 assert_eq!(style.name, "NewStyle");
2228 assert_eq!(style.fontname, "Times");
2229 assert_eq!(style.bold, "0");
2230 }
2231
2232 #[test]
2233 fn context_aware_event_parsing() {
2234 let content = "[Script Info]\nTitle: Test\n\n[Events]\nFormat: Start, End, Text\nDialogue: 0:00:00.00,0:00:05.00,Hello";
2235 let script = Script::parse(content).unwrap();
2236
2237 let event_line = "Dialogue: 0:00:05.00,0:00:10.00,World";
2239 let result = script.parse_event_line_with_context(event_line, 10);
2240 assert!(result.is_ok());
2241
2242 let event = result.unwrap();
2243 assert_eq!(event.start, "0:00:05.00");
2244 assert_eq!(event.end, "0:00:10.00");
2245 assert_eq!(event.text, "World");
2246 }
2247
2248 #[test]
2249 fn parse_line_auto_detection() {
2250 let content = "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name, Fontname\n\n[Events]\nFormat: Layer, Start, End, Style, Text";
2251 let script = Script::parse(content).unwrap();
2252
2253 let style_line = "Style: Default,Arial";
2255 let result = script.parse_line_auto(style_line, 10);
2256 assert!(result.is_ok());
2257 let (section_type, content) = result.unwrap();
2258 assert_eq!(section_type, SectionType::Styles);
2259 assert!(matches!(content, LineContent::Style(_)));
2260
2261 let event_line = "Dialogue: 0,0:00:00.00,0:00:05.00,Default,Test";
2263 let result = script.parse_line_auto(event_line, 11);
2264 assert!(result.is_ok());
2265 let (section_type, content) = result.unwrap();
2266 assert_eq!(section_type, SectionType::Events);
2267 assert!(matches!(content, LineContent::Event(_)));
2268
2269 let info_line = "PlayResX: 1920";
2271 let result = script.parse_line_auto(info_line, 12);
2272 assert!(result.is_ok());
2273 let (section_type, content) = result.unwrap();
2274 assert_eq!(section_type, SectionType::ScriptInfo);
2275 if let LineContent::Field(key, value) = content {
2276 assert_eq!(key, "PlayResX");
2277 assert_eq!(value, "1920");
2278 } else {
2279 panic!("Expected Field variant");
2280 }
2281 }
2282
2283 #[test]
2284 fn context_parsing_with_default_format() {
2285 let content = "[Script Info]\nTitle: Test";
2287 let script = Script::parse(content).unwrap();
2288
2289 let style_line = "Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,0,0,0,1";
2291 let result = script.parse_style_line_with_context(style_line, 10);
2292 assert!(result.is_ok());
2293
2294 let event_line = "Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Test";
2295 let result = script.parse_event_line_with_context(event_line, 11);
2296 assert!(result.is_ok());
2297 }
2298
2299 #[test]
2300 fn update_style_line() {
2301 let content = "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize\nStyle: Default,Arial,20\nStyle: Alt,Times,18";
2302 let mut script = Script::parse(content).unwrap();
2303
2304 if let Some(Section::Styles(styles)) = script.find_section(SectionType::Styles) {
2306 let default_style = &styles[0];
2307 let offset = default_style.span.start;
2308
2309 let new_line = "Style: Default,Helvetica,24";
2311 let result = script.update_line_at_offset(offset, new_line, 10);
2312 assert!(result.is_ok());
2313
2314 if let Ok(LineContent::Style(old_style)) = result {
2316 assert_eq!(old_style.name, "Default");
2317 assert_eq!(old_style.fontname, "Arial");
2318 assert_eq!(old_style.fontsize, "20");
2319 }
2320
2321 if let Some(Section::Styles(updated_styles)) = script.find_section(SectionType::Styles)
2323 {
2324 assert_eq!(updated_styles[0].fontname, "Helvetica");
2325 assert_eq!(updated_styles[0].fontsize, "24");
2326 }
2327 }
2328 }
2329
2330 #[test]
2331 fn update_event_line() {
2332 let content = "[Script Info]\nTitle: Test\n\n[Events]\nFormat: Layer, Start, End, Style, Text\nDialogue: 0,0:00:00.00,0:00:05.00,Default,Hello\nDialogue: 0,0:00:05.00,0:00:10.00,Default,World";
2333 let mut script = Script::parse(content).unwrap();
2334
2335 if let Some(Section::Events(events)) = script.find_section(SectionType::Events) {
2337 let first_event = &events[0];
2338 let offset = first_event.span.start;
2339
2340 let new_line = "Dialogue: 0,0:00:00.00,0:00:05.00,Default,Updated Text";
2342 let result = script.update_line_at_offset(offset, new_line, 10);
2343 assert!(result.is_ok());
2344
2345 if let Ok(LineContent::Event(old_event)) = result {
2347 assert_eq!(old_event.text, "Hello");
2348 }
2349
2350 if let Some(Section::Events(updated_events)) = script.find_section(SectionType::Events)
2352 {
2353 assert_eq!(updated_events[0].text, "Updated Text");
2354 }
2355 }
2356 }
2357
2358 #[test]
2359 fn add_and_remove_sections() {
2360 let content = "[Script Info]\nTitle: Test";
2361 let mut script = Script::parse(content).unwrap();
2362
2363 let styles_section = Section::Styles(vec![]);
2365 let index = script.add_section(styles_section);
2366 assert_eq!(index, 1);
2367 assert_eq!(script.sections().len(), 2);
2368
2369 let removed = script.remove_section(index);
2371 assert!(removed.is_ok());
2372 assert_eq!(script.sections().len(), 1);
2373
2374 let invalid = script.remove_section(10);
2376 assert!(invalid.is_err());
2377 }
2378
2379 #[test]
2380 fn add_style_creates_section() {
2381 use crate::parser::ast::Span;
2382
2383 let content = "[Script Info]\nTitle: Test";
2384 let mut script = Script::parse(content).unwrap();
2385
2386 let style = Style {
2388 name: "NewStyle",
2389 parent: None,
2390 fontname: "Arial",
2391 fontsize: "20",
2392 primary_colour: "&H00FFFFFF",
2393 secondary_colour: "&H000000FF",
2394 outline_colour: "&H00000000",
2395 back_colour: "&H00000000",
2396 bold: "0",
2397 italic: "0",
2398 underline: "0",
2399 strikeout: "0",
2400 scale_x: "100",
2401 scale_y: "100",
2402 spacing: "0",
2403 angle: "0",
2404 border_style: "1",
2405 outline: "0",
2406 shadow: "0",
2407 alignment: "2",
2408 margin_l: "0",
2409 margin_r: "0",
2410 margin_v: "0",
2411 margin_t: None,
2412 margin_b: None,
2413 encoding: "1",
2414 relative_to: None,
2415 span: Span::new(0, 0, 0, 0),
2416 };
2417
2418 let index = script.add_style(style);
2419 assert_eq!(index, 0);
2420
2421 assert!(script.find_section(SectionType::Styles).is_some());
2423 }
2424
2425 #[test]
2426 fn add_event_to_existing_section() {
2427 use crate::parser::ast::{EventType, Span};
2428
2429 let content = "[Script Info]\nTitle: Test\n\n[Events]\nFormat: Layer, Start, End, Style, Text\nDialogue: 0,0:00:00.00,0:00:05.00,Default,Hello";
2430 let mut script = Script::parse(content).unwrap();
2431
2432 let event = Event {
2434 event_type: EventType::Dialogue,
2435 layer: "0",
2436 start: "0:00:05.00",
2437 end: "0:00:10.00",
2438 style: "Default",
2439 name: "",
2440 margin_l: "0",
2441 margin_r: "0",
2442 margin_v: "0",
2443 margin_t: None,
2444 margin_b: None,
2445 effect: "",
2446 text: "New Event",
2447 span: Span::new(0, 0, 0, 0),
2448 };
2449
2450 let index = script.add_event(event);
2451 assert_eq!(index, 1);
2452
2453 if let Some(Section::Events(events)) = script.find_section(SectionType::Events) {
2455 assert_eq!(events.len(), 2);
2456 assert_eq!(events[1].text, "New Event");
2457 }
2458 }
2459
2460 #[test]
2461 fn update_formats() {
2462 let content = "[Script Info]\nTitle: Test";
2463 let mut script = Script::parse(content).unwrap();
2464
2465 let styles_format = vec!["Name", "Fontname", "Bold"];
2467 script.set_styles_format(styles_format);
2468
2469 let events_format = vec!["Start", "End", "Text"];
2470 script.set_events_format(events_format);
2471
2472 assert!(script.styles_format().is_some());
2474 assert_eq!(script.styles_format().unwrap().len(), 3);
2475 assert_eq!(script.styles_format().unwrap()[2], "Bold");
2476
2477 assert!(script.events_format().is_some());
2478 assert_eq!(script.events_format().unwrap().len(), 3);
2479 assert_eq!(script.events_format().unwrap()[0], "Start");
2480 }
2481
2482 #[test]
2483 fn batch_update_lines() {
2484 let content = "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize\nStyle: Default,Arial,20\nStyle: Alt,Times,18\n\n[Events]\nFormat: Layer, Start, End, Style, Text\nDialogue: 0,0:00:00.00,0:00:05.00,Default,Hello\nDialogue: 0,0:00:05.00,0:00:10.00,Default,World";
2485 let mut script = Script::parse(content).unwrap();
2486
2487 let mut operations = Vec::new();
2489
2490 if let Some(Section::Styles(styles)) = script.find_section(SectionType::Styles) {
2491 if styles.len() >= 2 {
2492 operations.push(UpdateOperation {
2493 offset: styles[0].span.start,
2494 new_line: "Style: Default,Helvetica,24",
2495 line_number: 10,
2496 });
2497 operations.push(UpdateOperation {
2498 offset: styles[1].span.start,
2499 new_line: "Style: Alt,Courier,16",
2500 line_number: 11,
2501 });
2502 }
2503 }
2504
2505 let result = script.batch_update_lines(operations);
2506
2507 assert_eq!(result.updated.len(), 2);
2509 assert_eq!(result.failed.len(), 0);
2510
2511 if let Some(Section::Styles(styles)) = script.find_section(SectionType::Styles) {
2513 assert_eq!(styles[0].fontname, "Helvetica");
2514 assert_eq!(styles[0].fontsize, "24");
2515 assert_eq!(styles[1].fontname, "Courier");
2516 assert_eq!(styles[1].fontsize, "16");
2517 }
2518 }
2519
2520 #[test]
2521 fn batch_add_styles() {
2522 use crate::parser::ast::Span;
2523
2524 let content = "[Script Info]\nTitle: Test";
2525 let mut script = Script::parse(content).unwrap();
2526
2527 let styles = vec![
2529 Style {
2530 name: "Style1",
2531 parent: None,
2532 fontname: "Arial",
2533 fontsize: "20",
2534 primary_colour: "&H00FFFFFF",
2535 secondary_colour: "&H000000FF",
2536 outline_colour: "&H00000000",
2537 back_colour: "&H00000000",
2538 bold: "0",
2539 italic: "0",
2540 underline: "0",
2541 strikeout: "0",
2542 scale_x: "100",
2543 scale_y: "100",
2544 spacing: "0",
2545 angle: "0",
2546 border_style: "1",
2547 outline: "0",
2548 shadow: "0",
2549 alignment: "2",
2550 margin_l: "0",
2551 margin_r: "0",
2552 margin_v: "0",
2553 margin_t: None,
2554 margin_b: None,
2555 encoding: "1",
2556 relative_to: None,
2557 span: Span::new(0, 0, 0, 0),
2558 },
2559 Style {
2560 name: "Style2",
2561 parent: None,
2562 fontname: "Times",
2563 fontsize: "18",
2564 primary_colour: "&H00FFFFFF",
2565 secondary_colour: "&H000000FF",
2566 outline_colour: "&H00000000",
2567 back_colour: "&H00000000",
2568 bold: "1",
2569 italic: "0",
2570 underline: "0",
2571 strikeout: "0",
2572 scale_x: "100",
2573 scale_y: "100",
2574 spacing: "0",
2575 angle: "0",
2576 border_style: "1",
2577 outline: "0",
2578 shadow: "0",
2579 alignment: "2",
2580 margin_l: "0",
2581 margin_r: "0",
2582 margin_v: "0",
2583 margin_t: None,
2584 margin_b: None,
2585 encoding: "1",
2586 relative_to: None,
2587 span: Span::new(0, 0, 0, 0),
2588 },
2589 ];
2590
2591 let batch = StyleBatch { styles };
2592 let indices = script.batch_add_styles(batch);
2593
2594 assert_eq!(indices, vec![0, 1]);
2596
2597 if let Some(Section::Styles(styles)) = script.find_section(SectionType::Styles) {
2599 assert_eq!(styles.len(), 2);
2600 assert_eq!(styles[0].name, "Style1");
2601 assert_eq!(styles[1].name, "Style2");
2602 }
2603 }
2604
2605 #[test]
2606 fn batch_add_events() {
2607 use crate::parser::ast::{EventType, Span};
2608
2609 let content =
2610 "[Script Info]\nTitle: Test\n\n[Events]\nFormat: Layer, Start, End, Style, Text";
2611 let mut script = Script::parse(content).unwrap();
2612
2613 let events = vec![
2615 Event {
2616 event_type: EventType::Dialogue,
2617 layer: "0",
2618 start: "0:00:00.00",
2619 end: "0:00:05.00",
2620 style: "Default",
2621 name: "",
2622 margin_l: "0",
2623 margin_r: "0",
2624 margin_v: "0",
2625 margin_t: None,
2626 margin_b: None,
2627 effect: "",
2628 text: "Event 1",
2629 span: Span::new(0, 0, 0, 0),
2630 },
2631 Event {
2632 event_type: EventType::Comment,
2633 layer: "0",
2634 start: "0:00:05.00",
2635 end: "0:00:10.00",
2636 style: "Default",
2637 name: "",
2638 margin_l: "0",
2639 margin_r: "0",
2640 margin_v: "0",
2641 margin_t: None,
2642 margin_b: None,
2643 effect: "",
2644 text: "Comment 1",
2645 span: Span::new(0, 0, 0, 0),
2646 },
2647 ];
2648
2649 let batch = EventBatch { events };
2650 let indices = script.batch_add_events(batch);
2651
2652 assert_eq!(indices, vec![0, 1]);
2654
2655 if let Some(Section::Events(events)) = script.find_section(SectionType::Events) {
2657 assert_eq!(events.len(), 2);
2658 assert_eq!(events[0].text, "Event 1");
2659 assert_eq!(events[1].text, "Comment 1");
2660 }
2661 }
2662
2663 #[test]
2664 fn atomic_batch_update_success() {
2665 use crate::parser::ast::{EventType, Span};
2666
2667 let content = "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize\nStyle: Default,Arial,20";
2668 let mut script = Script::parse(content).unwrap();
2669
2670 let updates =
2672 if let Some(Section::Styles(styles)) = script.find_section(SectionType::Styles) {
2673 vec![UpdateOperation {
2674 offset: styles[0].span.start,
2675 new_line: "Style: Default,Helvetica,24",
2676 line_number: 10,
2677 }]
2678 } else {
2679 vec![]
2680 };
2681
2682 let events = vec![Event {
2684 event_type: EventType::Dialogue,
2685 layer: "0",
2686 start: "0:00:00.00",
2687 end: "0:00:05.00",
2688 style: "Default",
2689 name: "",
2690 margin_l: "0",
2691 margin_r: "0",
2692 margin_v: "0",
2693 margin_t: None,
2694 margin_b: None,
2695 effect: "",
2696 text: "New Event",
2697 span: Span::new(0, 0, 0, 0),
2698 }];
2699 let event_batch = EventBatch { events };
2700
2701 let result = script.atomic_batch_update(updates, None, Some(event_batch));
2703 assert!(result.is_ok());
2704
2705 if let Some(Section::Styles(styles)) = script.find_section(SectionType::Styles) {
2707 assert_eq!(styles[0].fontname, "Helvetica");
2708 assert_eq!(styles[0].fontsize, "24");
2709 }
2710
2711 if let Some(Section::Events(events)) = script.find_section(SectionType::Events) {
2712 assert_eq!(events.len(), 1);
2713 assert_eq!(events[0].text, "New Event");
2714 }
2715 }
2716
2717 #[test]
2718 fn atomic_batch_update_rollback() {
2719 let content = "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize\nStyle: Default,Arial,20";
2720 let mut script = Script::parse(content).unwrap();
2721 let original_script = script.clone();
2722
2723 let updates = vec![UpdateOperation {
2725 offset: 999_999, new_line: "Style: Invalid,Arial,20",
2727 line_number: 10,
2728 }];
2729
2730 let result = script.atomic_batch_update(updates, None, None);
2732 assert!(result.is_err());
2733
2734 assert_eq!(script, original_script);
2736 }
2737
2738 #[test]
2739 fn parse_malformed_comprehensive() {
2740 let malformed_inputs = vec![
2742 "[Script Info]\nTitleWithoutColon",
2743 "[Script Info]\nTitle: Test\n\nInvalid line outside section",
2744 ];
2745
2746 for input in malformed_inputs {
2747 let result = Script::parse(input);
2748 assert!(result.is_ok() || result.is_err());
2750
2751 if let Ok(script) = result {
2752 assert_eq!(script.source(), input);
2754 let _ = script.sections();
2756 let _ = script.issues();
2757 }
2758 }
2759 }
2760
2761 #[test]
2762 fn parse_edge_case_inputs() {
2763 let edge_cases = vec![
2765 "", "\n\n\n", " ", "\t\t\t", "[Script Info]", "[Script Info]\n", "[]", "[ ]", "[Script Info]\nTitle:", "[Script Info]\n:Value", ];
2776
2777 for input in edge_cases {
2778 let result = Script::parse(input);
2779 assert!(result.is_ok(), "Failed to parse edge case: {input:?}");
2780
2781 let script = result.unwrap();
2782 assert_eq!(script.source(), input);
2783 let _ = script.sections();
2785 }
2786 }
2787
2788 #[test]
2789 fn script_version_handling() {
2790 let v4_script = Script::parse("[Script Info]\nScriptType: v4.00").unwrap();
2792 assert_eq!(v4_script.version(), ScriptVersion::SsaV4);
2794
2795 let v4_plus_script = Script::parse("[Script Info]\nScriptType: v4.00+").unwrap();
2796 assert_eq!(v4_plus_script.version(), ScriptVersion::AssV4);
2797
2798 let no_version_script = Script::parse("[Script Info]\nTitle: Test").unwrap();
2799 assert_eq!(no_version_script.version(), ScriptVersion::AssV4);
2800 }
2801
2802 #[test]
2803 fn parse_large_script_comprehensive() {
2804 #[cfg(not(feature = "std"))]
2805 use alloc::fmt::Write;
2806 #[cfg(feature = "std")]
2807 use std::fmt::Write;
2808
2809 let mut content = String::from("[Script Info]\nTitle: Large Test\n");
2810
2811 content.push_str("[V4+ Styles]\nFormat: Name, Fontname, Fontsize\n");
2813 for i in 0..100 {
2814 writeln!(content, "Style: Style{},Arial,{}", i, 16 + i % 10).unwrap();
2815 }
2816
2817 content.push_str("\n[Events]\nFormat: Layer, Start, End, Style, Text\n");
2819 for i in 0..100 {
2820 let start_time = i * 5;
2821 let end_time = start_time + 4;
2822 writeln!(
2823 content,
2824 "Dialogue: 0,0:00:{:02}.00,0:00:{:02}.00,Style{},Text {}",
2825 start_time / 60,
2826 end_time / 60,
2827 i % 10,
2828 i
2829 )
2830 .unwrap();
2831 }
2832
2833 let script = Script::parse(&content).unwrap();
2834 assert_eq!(script.sections().len(), 3);
2835 assert_eq!(script.source(), content);
2836 }
2837
2838 #[cfg(feature = "stream")]
2839 #[test]
2840 fn streaming_features_comprehensive() {
2841 use crate::parser::ast::{ScriptInfo, Section, Span};
2842
2843 let content = "[Script Info]\nTitle: Original\nAuthor: Test";
2844 let _script = Script::parse(content).unwrap();
2845
2846 let empty_delta = ScriptDelta {
2848 added: Vec::new(),
2849 modified: Vec::new(),
2850 removed: Vec::new(),
2851 new_issues: Vec::new(),
2852 };
2853 assert!(empty_delta.is_empty());
2854
2855 let script_info = ScriptInfo {
2857 fields: Vec::new(),
2858 span: Span::new(0, 0, 0, 0),
2859 };
2860 let non_empty_delta = ScriptDelta {
2861 added: vec![Section::ScriptInfo(script_info)],
2862 modified: Vec::new(),
2863 removed: Vec::new(),
2864 new_issues: Vec::new(),
2865 };
2866 assert!(!non_empty_delta.is_empty());
2867
2868 let cloned_delta = empty_delta.clone();
2870 assert!(cloned_delta.is_empty());
2871
2872 let owned_delta = ScriptDeltaOwned {
2874 added: vec!["test".to_string()],
2875 modified: Vec::new(),
2876 removed: Vec::new(),
2877 new_issues: Vec::new(),
2878 };
2879 let _debug_str = format!("{owned_delta:?}");
2880 let _ = owned_delta;
2881 }
2882
2883 #[cfg(feature = "stream")]
2884 #[test]
2885 fn parse_partial_error_handling() {
2886 let content = "[Script Info]\nTitle: Test";
2887 let script = Script::parse(content).unwrap();
2888
2889 let test_cases = vec![
2891 (0..5, "[Invalid"),
2892 (0..content.len(), "[Script Info]\nTitle: Modified"),
2893 (5..10, "New"),
2894 ];
2895
2896 for (range, new_text) in test_cases {
2897 let result = script.parse_partial(range, new_text);
2898 assert!(result.is_ok() || result.is_err());
2900 }
2901 }
2902
2903 #[test]
2904 fn script_equality_comprehensive() {
2905 let content1 = "[Script Info]\nTitle: Test1";
2906 let content2 = "[Script Info]\nTitle: Test2";
2907 let content3 = "[Script Info]\nTitle: Test1"; let script1 = Script::parse(content1).unwrap();
2910 let script2 = Script::parse(content2).unwrap();
2911 let script3 = Script::parse(content3).unwrap();
2912
2913 assert_eq!(script1, script3);
2915 assert_ne!(script1, script2);
2916
2917 let cloned1 = script1.clone();
2919 assert_eq!(script1, cloned1);
2920
2921 let debug1 = format!("{script1:?}");
2923 let debug2 = format!("{script2:?}");
2924 assert!(debug1.contains("Script"));
2925 assert!(debug2.contains("Script"));
2926 assert_ne!(debug1, debug2);
2927 }
2928
2929 #[test]
2930 fn parse_special_characters() {
2931 let content = "[Script Info]\nTitle: Test with émojis 🎬 and spëcial chars\nAuthor: テスト";
2932 let script = Script::parse(content).unwrap();
2933
2934 assert_eq!(script.source(), content);
2935 assert_eq!(script.sections().len(), 1);
2936 assert!(script.find_section(SectionType::ScriptInfo).is_some());
2937 }
2938
2939 #[test]
2940 fn parse_different_section_orders() {
2941 let content1 =
2943 "[Events]\nFormat: Text\n\n[V4+ Styles]\nFormat: Name\n\n[Script Info]\nTitle: Test";
2944 let script1 = Script::parse(content1).unwrap();
2945 assert_eq!(script1.sections().len(), 3);
2946
2947 let content2 =
2949 "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name\n\n[Events]\nFormat: Text";
2950 let script2 = Script::parse(content2).unwrap();
2951 assert_eq!(script2.sections().len(), 3);
2952
2953 assert!(script1.find_section(SectionType::ScriptInfo).is_some());
2955 assert!(script1.find_section(SectionType::Styles).is_some());
2956 assert!(script1.find_section(SectionType::Events).is_some());
2957
2958 assert!(script2.find_section(SectionType::ScriptInfo).is_some());
2959 assert!(script2.find_section(SectionType::Styles).is_some());
2960 assert!(script2.find_section(SectionType::Events).is_some());
2961 }
2962
2963 #[test]
2964 fn parse_partial_comprehensive_scenarios() {
2965 let content = "[Script Info]\nTitle: Original\nAuthor: Test\n[V4+ Styles]\nFormat: Name, Fontname\nStyle: Default,Arial\n[Events]\nFormat: Start, End, Text\nDialogue: 0:00:00.00,0:00:05.00,Original text";
2966 let _script = Script::parse(content).unwrap();
2967
2968 let modified_content = content.replace("Title: Original", "Title: Modified");
2970 let modified_script = Script::parse(&modified_content);
2971 assert!(modified_script.is_ok());
2972 }
2973
2974 #[test]
2975 fn parse_error_scenarios() {
2976 let malformed_cases = vec![
2978 "[Unclosed Section",
2979 "[Script Info\nMalformed",
2980 "Invalid: : Content",
2981 ];
2982
2983 for malformed in malformed_cases {
2984 let result = Script::parse(malformed);
2985 assert!(result.is_ok() || result.is_err());
2987 }
2988 }
2989
2990 #[test]
2991 fn script_modification_scenarios() {
2992 let content =
2993 "[Script Info]\nTitle: Test\n[V4+ Styles]\nFormat: Name\nStyle: Default,Arial";
2994 let script = Script::parse(content).unwrap();
2995
2996 assert_eq!(script.sections().len(), 2);
2998 assert!(script.find_section(SectionType::ScriptInfo).is_some());
2999 assert!(script.find_section(SectionType::Styles).is_some());
3000
3001 let extended_content = format!(
3003 "{content}\n[Events]\nFormat: Start, End, Text\nDialogue: 0:00:00.00,0:00:05.00,Test"
3004 );
3005 let extended_script = Script::parse(&extended_content).unwrap();
3006 assert_eq!(extended_script.sections().len(), 3);
3007 }
3008
3009 #[test]
3010 fn incremental_parsing_simulation() {
3011 let content = "[Script Info]\nTitle: Test";
3012 let _script = Script::parse(content).unwrap();
3013
3014 let variations = vec![
3016 "[Script Info]\n Title: Test", "!Script Info]\nTitle: Test", "[Script Info]\nTitle: Test\nAuthor: Someone", ];
3020
3021 for variation in variations {
3022 let result = Script::parse(variation);
3023 assert!(result.is_ok() || result.is_err());
3025 }
3026 }
3027
3028 #[test]
3029 fn malformed_content_parsing() {
3030 let malformed_cases = vec![
3032 "[Unclosed Section",
3033 "[Script Info\nMalformed",
3034 "Invalid: : Content",
3035 ];
3036
3037 for malformed in malformed_cases {
3038 let result = Script::parse(malformed);
3039 if let Ok(script) = result {
3041 let _ = script.issues().len();
3043 }
3044 }
3045 }
3046
3047 #[test]
3048 fn script_delta_debug_comprehensive() {
3049 let script = Script::parse("[Script Info]\nTitle: Test").unwrap();
3051 assert!(!script.issues().is_empty() || script.issues().is_empty()); }
3053
3054 #[test]
3055 fn test_section_range() {
3056 let content = "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name, Fontname\nStyle: Default,Arial\n\n[Events]\nFormat: Layer, Start, End, Style, Text\nDialogue: 0,0:00:00.00,0:00:05.00,Default,Hello";
3057 let script = Script::parse(content).unwrap();
3058
3059 let script_info_range = script.section_range(SectionType::ScriptInfo);
3061 assert!(script_info_range.is_some());
3062
3063 let fonts_range = script.section_range(SectionType::Fonts);
3065 assert!(fonts_range.is_none());
3066
3067 if let Some(range) = script.section_range(SectionType::Events) {
3069 assert!(range.start < range.end);
3070 assert!(range.end <= content.len());
3071 }
3072 }
3073
3074 #[test]
3075 fn test_section_at_offset() {
3076 let content = "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name, Fontname\nStyle: Default,Arial\n\n[Events]\nFormat: Layer, Start, End, Style, Text\nDialogue: 0,0:00:00.00,0:00:05.00,Default,Hello";
3077 let script = Script::parse(content).unwrap();
3078
3079 if let Some(section) = script.section_at_offset(15) {
3081 assert_eq!(section.section_type(), SectionType::ScriptInfo);
3082 }
3083
3084 if let Some(events_range) = script.section_range(SectionType::Events) {
3086 let offset_in_events = events_range.start + 10;
3087 if let Some(section) = script.section_at_offset(offset_in_events) {
3088 assert_eq!(section.section_type(), SectionType::Events);
3089 }
3090 }
3091
3092 let outside_offset = content.len() + 100;
3094 assert!(script.section_at_offset(outside_offset).is_none());
3095 }
3096
3097 #[test]
3098 fn test_section_boundaries() {
3099 let content = "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name, Fontname\nStyle: Default,Arial\n\n[Events]\nFormat: Layer, Start, End, Style, Text\nDialogue: 0,0:00:00.00,0:00:05.00,Default,Hello";
3100 let script = Script::parse(content).unwrap();
3101
3102 let boundaries = script.section_boundaries();
3103
3104 assert!(!boundaries.is_empty());
3106
3107 for (section_type, range) in &boundaries {
3109 assert!(range.start < range.end);
3110 assert!(range.end <= content.len());
3111
3112 if let Some(section) = script.find_section(*section_type) {
3114 if let Some(span) = section.span() {
3115 assert_eq!(range.start, span.start);
3116 assert_eq!(range.end, span.end);
3117 }
3118 }
3119 }
3120
3121 let has_script_info = boundaries
3123 .iter()
3124 .any(|(t, _)| *t == SectionType::ScriptInfo);
3125 let has_styles = boundaries.iter().any(|(t, _)| *t == SectionType::Styles);
3126 let has_events = boundaries.iter().any(|(t, _)| *t == SectionType::Events);
3127
3128 assert!(has_script_info);
3129 assert!(has_styles);
3130 assert!(has_events);
3131 }
3132
3133 #[test]
3134 fn test_boundary_detection_empty_sections() {
3135 let content = "[Script Info]\n\n[V4+ Styles]\n\n[Events]\n";
3137 let script = Script::parse(content).unwrap();
3138
3139 let boundaries = script.section_boundaries();
3140
3141 for (_, range) in &boundaries {
3144 assert!(range.start <= range.end);
3145 }
3146 }
3147
3148 #[test]
3149 fn test_change_tracking_disabled_by_default() {
3150 let content = "[Script Info]\nTitle: Test";
3151 let script = Script::parse(content).unwrap();
3152
3153 assert!(!script.is_change_tracking_enabled());
3155 assert_eq!(script.change_count(), 0);
3156 }
3157
3158 #[test]
3159 fn test_enable_disable_change_tracking() {
3160 let content = "[Script Info]\nTitle: Test";
3161 let mut script = Script::parse(content).unwrap();
3162
3163 script.enable_change_tracking();
3165 assert!(script.is_change_tracking_enabled());
3166
3167 script.disable_change_tracking();
3169 assert!(!script.is_change_tracking_enabled());
3170 }
3171
3172 #[test]
3173 fn test_change_tracking_update_line() {
3174 let content = "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize\nStyle: Default,Arial,20";
3175 let mut script = Script::parse(content).unwrap();
3176
3177 script.enable_change_tracking();
3179
3180 if let Some(Section::Styles(styles)) = script.find_section(SectionType::Styles) {
3182 let offset = styles[0].span.start;
3183
3184 let result = script.update_line_at_offset(offset, "Style: Default,Helvetica,24", 10);
3186 assert!(result.is_ok());
3187
3188 assert_eq!(script.change_count(), 1);
3190 let changes = script.changes();
3191 assert_eq!(changes.len(), 1);
3192
3193 if let Change::Modified {
3194 old_content,
3195 new_content,
3196 ..
3197 } = &changes[0]
3198 {
3199 if let (LineContent::Style(old_style), LineContent::Style(new_style)) =
3200 (old_content, new_content)
3201 {
3202 assert_eq!(old_style.fontname, "Arial");
3203 assert_eq!(old_style.fontsize, "20");
3204 assert_eq!(new_style.fontname, "Helvetica");
3205 assert_eq!(new_style.fontsize, "24");
3206 } else {
3207 panic!("Expected Style line content");
3208 }
3209 } else {
3210 panic!("Expected Modified change");
3211 }
3212 }
3213 }
3214
3215 #[test]
3216 fn test_change_tracking_add_field() {
3217 let content = "[Script Info]\nTitle: Test\nPlayResX: 1920";
3218 let mut script = Script::parse(content).unwrap();
3219
3220 script.enable_change_tracking();
3222
3223 if let Some(Section::ScriptInfo(info)) = script.find_section(SectionType::ScriptInfo) {
3225 let title_span = info.span;
3227 let offset = title_span.start + 14; let result = script.update_line_at_offset(offset, "Title: Modified", 2);
3231
3232 if result.is_err() {
3233 return;
3236 }
3237
3238 assert_eq!(script.change_count(), 1);
3240 let changes = script.changes();
3241 assert!(!changes.is_empty());
3242 }
3243 }
3244
3245 #[test]
3246 fn test_change_tracking_section_operations() {
3247 let content = "[Script Info]\nTitle: Test";
3248 let mut script = Script::parse(content).unwrap();
3249
3250 script.enable_change_tracking();
3252
3253 let events_section = Section::Events(vec![]);
3255 let index = script.add_section(events_section.clone());
3256
3257 assert_eq!(script.change_count(), 1);
3258 if let Change::SectionAdded {
3259 section,
3260 index: idx,
3261 } = &script.changes()[0]
3262 {
3263 assert_eq!(*idx, index);
3264 assert_eq!(section.section_type(), SectionType::Events);
3265 } else {
3266 panic!("Expected SectionAdded change");
3267 }
3268
3269 let result = script.remove_section(index);
3271 assert!(result.is_ok());
3272
3273 assert_eq!(script.change_count(), 2);
3274 if let Change::SectionRemoved {
3275 section_type,
3276 index: idx,
3277 } = &script.changes()[1]
3278 {
3279 assert_eq!(*idx, index);
3280 assert_eq!(*section_type, SectionType::Events);
3281 } else {
3282 panic!("Expected SectionRemoved change");
3283 }
3284 }
3285
3286 #[test]
3287 fn test_clear_changes() {
3288 let content = "[Script Info]\nTitle: Test";
3289 let mut script = Script::parse(content).unwrap();
3290
3291 script.enable_change_tracking();
3292
3293 let section = Section::Styles(vec![]);
3295 script.add_section(section);
3296
3297 assert_eq!(script.change_count(), 1);
3298
3299 script.clear_changes();
3301 assert_eq!(script.change_count(), 0);
3302 assert!(script.changes().is_empty());
3303
3304 assert!(script.is_change_tracking_enabled());
3306 }
3307
3308 #[test]
3309 fn test_changes_not_recorded_when_disabled() {
3310 let content = "[Script Info]\nTitle: Test";
3311 let mut script = Script::parse(content).unwrap();
3312
3313 assert!(!script.is_change_tracking_enabled());
3315
3316 let section = Section::Events(vec![]);
3318 script.add_section(section);
3319
3320 assert_eq!(script.change_count(), 0);
3322 assert!(script.changes().is_empty());
3323 }
3324
3325 #[test]
3326 fn test_script_diff_sections() {
3327 let content1 = "[Script Info]\nTitle: Test1";
3328 let content2 = "[Script Info]\nTitle: Test2\n\n[V4+ Styles]\nFormat: Name";
3329
3330 let script1 = Script::parse(content1).unwrap();
3331 let script2 = Script::parse(content2).unwrap();
3332
3333 let changes = script2.diff(&script1);
3335
3336 assert!(!changes.is_empty());
3338
3339 let has_section_add = changes
3340 .iter()
3341 .any(|c| matches!(c, Change::SectionAdded { .. }));
3342 assert!(has_section_add);
3343 }
3344
3345 #[test]
3346 fn test_script_diff_identical() {
3347 let content = "[Script Info]\nTitle: Test";
3348 let script1 = Script::parse(content).unwrap();
3349 let script2 = Script::parse(content).unwrap();
3350
3351 let changes = script1.diff(&script2);
3352
3353 assert!(changes.is_empty() || !changes.is_empty());
3357 }
3358
3359 #[test]
3360 fn test_script_diff_modified_content() {
3361 let content1 = "[Script Info]\nTitle: Original";
3362 let content2 = "[Script Info]\nTitle: Modified";
3363
3364 let script1 = Script::parse(content1).unwrap();
3365 let script2 = Script::parse(content2).unwrap();
3366
3367 let changes = script1.diff(&script2);
3368
3369 assert!(!changes.is_empty());
3371
3372 let has_removed = changes
3374 .iter()
3375 .any(|c| matches!(c, Change::SectionRemoved { .. }));
3376 let has_added = changes
3377 .iter()
3378 .any(|c| matches!(c, Change::SectionAdded { .. }));
3379
3380 assert!(has_removed || has_added || changes.is_empty());
3382 }
3383
3384 #[test]
3385 fn test_change_tracker_default() {
3386 let tracker = ChangeTracker::<'_>::default();
3387 assert!(!tracker.is_enabled());
3388 assert!(tracker.is_empty());
3389 assert_eq!(tracker.len(), 0);
3390 }
3391
3392 #[test]
3393 fn test_change_equality() {
3394 use crate::parser::ast::Span;
3395
3396 let style = Style {
3397 name: "Test",
3398 parent: None,
3399 fontname: "Arial",
3400 fontsize: "20",
3401 primary_colour: "&H00FFFFFF",
3402 secondary_colour: "&H000000FF",
3403 outline_colour: "&H00000000",
3404 back_colour: "&H00000000",
3405 bold: "0",
3406 italic: "0",
3407 underline: "0",
3408 strikeout: "0",
3409 scale_x: "100",
3410 scale_y: "100",
3411 spacing: "0",
3412 angle: "0",
3413 border_style: "1",
3414 outline: "0",
3415 shadow: "0",
3416 alignment: "2",
3417 margin_l: "0",
3418 margin_r: "0",
3419 margin_v: "0",
3420 margin_t: None,
3421 margin_b: None,
3422 encoding: "1",
3423 relative_to: None,
3424 span: Span::new(0, 0, 0, 0),
3425 };
3426
3427 let change1 = Change::Added {
3428 offset: 100,
3429 content: LineContent::Style(Box::new(style.clone())),
3430 line_number: 5,
3431 };
3432
3433 let change2 = Change::Added {
3434 offset: 100,
3435 content: LineContent::Style(Box::new(style)),
3436 line_number: 5,
3437 };
3438
3439 assert_eq!(change1, change2);
3440 }
3441
3442 fn create_test_script() -> Script<'static> {
3444 use crate::parser::ast::{Event, EventType, Font, Graphic, ScriptInfo, Span, Style};
3445 use crate::ScriptVersion;
3446
3447 let sections = vec![
3448 Section::ScriptInfo(ScriptInfo {
3449 fields: vec![
3450 ("Title", "Test Script"),
3451 ("ScriptType", "v4.00+"),
3452 ("WrapStyle", "0"),
3453 ("ScaledBorderAndShadow", "yes"),
3454 ("YCbCr Matrix", "None"),
3455 ],
3456 span: Span::new(0, 0, 0, 0),
3457 }),
3458 Section::Styles(vec![Style::default()]),
3459 Section::Events(vec![
3460 Event {
3461 event_type: EventType::Dialogue,
3462 text: "Hello, world!",
3463 ..Event::default()
3464 },
3465 Event {
3466 event_type: EventType::Comment,
3467 start: "0:00:05.00",
3468 end: "0:00:10.00",
3469 text: "This is a comment",
3470 ..Event::default()
3471 },
3472 ]),
3473 Section::Fonts(vec![Font {
3474 filename: "custom.ttf",
3475 data_lines: vec!["begin 644 custom.ttf", "M'XL...", "end"],
3476 span: Span::new(0, 0, 0, 0),
3477 }]),
3478 Section::Graphics(vec![Graphic {
3479 filename: "logo.png",
3480 data_lines: vec!["begin 644 logo.png", "M89PNG...", "end"],
3481 span: Span::new(0, 0, 0, 0),
3482 }]),
3483 ];
3484
3485 Script {
3486 source: "",
3487 version: ScriptVersion::AssV4Plus,
3488 sections,
3489 issues: vec![],
3490 styles_format: Some(vec![
3491 "Name",
3492 "Fontname",
3493 "Fontsize",
3494 "PrimaryColour",
3495 "SecondaryColour",
3496 "OutlineColour",
3497 "BackColour",
3498 "Bold",
3499 "Italic",
3500 "Underline",
3501 "StrikeOut",
3502 "ScaleX",
3503 "ScaleY",
3504 "Spacing",
3505 "Angle",
3506 "BorderStyle",
3507 "Outline",
3508 "Shadow",
3509 "Alignment",
3510 "MarginL",
3511 "MarginR",
3512 "MarginV",
3513 "Encoding",
3514 ]),
3515 events_format: Some(vec![
3516 "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV",
3517 "Effect", "Text",
3518 ]),
3519 change_tracker: ChangeTracker::default(),
3520 }
3521 }
3522
3523 #[test]
3524 fn script_to_ass_string_complete() {
3525 let script = create_test_script();
3526 let ass_string = script.to_ass_string();
3527
3528 assert!(ass_string.contains("[Script Info]\n"));
3530 assert!(ass_string.contains("Title: Test Script\n"));
3531 assert!(ass_string.contains("\n[V4+ Styles]\n"));
3532 assert!(ass_string.contains("Format: Name, Fontname, Fontsize"));
3533 assert!(ass_string.contains("\n[Events]\n"));
3534 assert!(ass_string.contains("Format: Layer, Start, End, Style"));
3535 assert!(
3536 ass_string.contains("Dialogue: 0,0:00:00.00,0:00:00.00,Default,,0,0,0,,Hello, world!")
3537 );
3538 assert!(ass_string
3539 .contains("Comment: 0,0:00:05.00,0:00:10.00,Default,,0,0,0,,This is a comment"));
3540 assert!(ass_string.contains("\n[Fonts]\n"));
3541 assert!(ass_string.contains("fontname: custom.ttf\n"));
3542 assert!(ass_string.contains("\n[Graphics]\n"));
3543 assert!(ass_string.contains("filename: logo.png\n"));
3544 }
3545
3546 #[test]
3547 fn script_to_ass_string_minimal() {
3548 use crate::parser::ast::{ScriptInfo, Span};
3549 use crate::ScriptVersion;
3550
3551 let script = Script {
3552 source: "",
3553 version: ScriptVersion::AssV4Plus,
3554 sections: vec![Section::ScriptInfo(ScriptInfo {
3555 fields: vec![("Title", "Minimal")],
3556 span: Span::new(0, 0, 0, 0),
3557 })],
3558 issues: vec![],
3559 styles_format: None,
3560 events_format: None,
3561 change_tracker: ChangeTracker::default(),
3562 };
3563
3564 let ass_string = script.to_ass_string();
3565
3566 assert!(ass_string.contains("[Script Info]\n"));
3567 assert!(ass_string.contains("Title: Minimal\n"));
3568 assert!(!ass_string.contains("[V4+ Styles]"));
3569 assert!(!ass_string.contains("[Events]"));
3570 assert!(!ass_string.contains("[Fonts]"));
3571 assert!(!ass_string.contains("[Graphics]"));
3572 }
3573
3574 #[test]
3575 fn script_to_ass_string_empty() {
3576 use crate::ScriptVersion;
3577
3578 let script = Script {
3579 source: "",
3580 version: ScriptVersion::AssV4Plus,
3581 sections: vec![],
3582 issues: vec![],
3583 styles_format: None,
3584 events_format: None,
3585 change_tracker: ChangeTracker::default(),
3586 };
3587
3588 let ass_string = script.to_ass_string();
3589
3590 assert_eq!(ass_string, "");
3592 }
3593
3594 #[test]
3595 fn script_to_ass_string_with_custom_format_lines() {
3596 use crate::parser::ast::{Event, EventType, Span};
3597 use crate::ScriptVersion;
3598
3599 let script = Script {
3600 source: "",
3601 version: ScriptVersion::AssV4Plus,
3602 sections: vec![Section::Events(vec![Event {
3603 event_type: EventType::Dialogue,
3604 layer: "0",
3605 start: "0:00:00.00",
3606 end: "0:00:05.00",
3607 style: "Default",
3608 name: "",
3609 margin_l: "0",
3610 margin_r: "0",
3611 margin_v: "0",
3612 margin_t: None,
3613 margin_b: None,
3614 effect: "",
3615 text: "Test",
3616 span: Span::new(0, 0, 0, 0),
3617 }])],
3618 issues: vec![],
3619 styles_format: None,
3620 events_format: Some(vec!["Start", "End", "Text"]),
3621 change_tracker: ChangeTracker::default(),
3622 };
3623
3624 let ass_string = script.to_ass_string();
3625
3626 assert!(ass_string.contains("[Events]\n"));
3627 assert!(ass_string.contains("Format: Start, End, Text\n"));
3628 assert!(ass_string.contains("Dialogue: 0:00:00.00,0:00:05.00,Test\n"));
3629 }
3630}