1use super::errors::{EditorError, Result};
7use super::history::UndoManager;
8use super::position::{LineColumn, Position, Range};
9use crate::commands::CommandResult;
10#[cfg(feature = "stream")]
11use ass_core::parser::script::ScriptDeltaOwned;
12use ass_core::parser::{ast::Section, Script};
13use core::ops::Range as StdRange;
14
15#[cfg(feature = "std")]
16use std::sync::Arc;
17
18#[cfg(not(feature = "std"))]
19use alloc::{
20 format,
21 string::{String, ToString},
22 vec,
23 vec::Vec,
24};
25
26#[cfg(feature = "std")]
27use std::sync::mpsc::Sender;
28
29#[cfg(feature = "std")]
30use crate::events::DocumentEvent;
31
32#[cfg(feature = "std")]
33type EventSender = Sender<DocumentEvent>;
34
35#[derive(Debug)]
40pub struct EditorDocument {
41 #[cfg(feature = "rope")]
43 text_rope: ropey::Rope,
44
45 #[cfg(not(feature = "rope"))]
47 text_content: String,
48
49 id: String,
51
52 modified: bool,
54
55 file_path: Option<String>,
57
58 #[cfg(feature = "plugins")]
60 registry_integration: Option<Arc<crate::extensions::registry_integration::RegistryIntegration>>,
61
62 history: UndoManager,
64
65 #[cfg(feature = "std")]
67 event_tx: Option<EventSender>,
68
69 #[cfg(feature = "stream")]
71 incremental_parser: crate::core::incremental::IncrementalParser,
72
73 validator: crate::utils::validator::LazyValidator,
75}
76
77impl EditorDocument {
78 pub fn new() -> Self {
80 Self {
81 #[cfg(feature = "rope")]
82 text_rope: ropey::Rope::new(),
83 #[cfg(not(feature = "rope"))]
84 text_content: String::new(),
85 id: Self::generate_id(),
86 modified: false,
87 file_path: None,
88 #[cfg(feature = "plugins")]
89 registry_integration: None,
90 history: UndoManager::new(),
91 #[cfg(feature = "std")]
92 event_tx: None,
93 #[cfg(feature = "stream")]
94 incremental_parser: crate::core::incremental::IncrementalParser::new(),
95 validator: crate::utils::validator::LazyValidator::new(),
96 }
97 }
98
99 #[cfg(feature = "std")]
101 pub fn with_event_channel(event_tx: EventSender) -> Self {
102 let mut doc = Self::new();
103 doc.event_tx = Some(event_tx);
104 doc
105 }
106
107 #[cfg(feature = "std")]
109 pub fn from_file(path: &str) -> Result<Self> {
110 use std::fs;
111 let content = fs::read_to_string(path).map_err(|e| EditorError::IoError(e.to_string()))?;
112 let mut doc = Self::from_content(&content)?;
113 doc.file_path = Some(path.to_string());
114 Ok(doc)
115 }
116
117 #[cfg(feature = "std")]
119 pub fn save(&mut self) -> Result<()> {
120 if let Some(path) = self.file_path.clone() {
121 self.save_to_file(&path)
122 } else {
123 Err(EditorError::IoError(
124 "No file path set for document".to_string(),
125 ))
126 }
127 }
128
129 #[cfg(feature = "std")]
131 pub fn save_to_file(&mut self, path: &str) -> Result<()> {
132 use std::fs;
133 let content = self.text();
134 fs::write(path, content).map_err(|e| EditorError::IoError(e.to_string()))?;
135 self.modified = false;
136 self.file_path = Some(path.to_string());
137 Ok(())
138 }
139
140 pub fn with_id(id: String) -> Self {
142 let mut doc = Self::new();
143 doc.id = id;
144 doc
145 }
146
147 #[cfg(feature = "std")]
149 fn emit(&mut self, event: DocumentEvent) {
150 if let Some(tx) = &mut self.event_tx {
151 let _ = tx.send(event);
152 }
153 }
154
155 #[cfg(feature = "std")]
157 pub fn set_event_channel(&mut self, event_tx: EventSender) {
158 self.event_tx = Some(event_tx);
159 }
160
161 #[cfg(feature = "std")]
163 pub fn has_event_channel(&self) -> bool {
164 self.event_tx.is_some()
165 }
166
167 pub fn from_content(content: &str) -> Result<Self> {
198 let _ = Script::parse(content).map_err(EditorError::from)?;
200
201 #[cfg(feature = "stream")]
202 let mut incremental_parser = crate::core::incremental::IncrementalParser::new();
203 #[cfg(feature = "stream")]
204 incremental_parser.initialize_cache(content);
205
206 Ok(Self {
207 #[cfg(feature = "rope")]
208 text_rope: ropey::Rope::from_str(content),
209 #[cfg(not(feature = "rope"))]
210 text_content: content.to_string(),
211 id: Self::generate_id(),
212 modified: false,
213 file_path: None,
214 #[cfg(feature = "plugins")]
215 registry_integration: None,
216 history: UndoManager::new(),
217 #[cfg(feature = "std")]
218 event_tx: None,
219 #[cfg(feature = "stream")]
220 incremental_parser,
221 validator: crate::utils::validator::LazyValidator::new(),
222 })
223 }
224
225 pub fn parse_script_with<F, R>(&self, f: F) -> Result<R>
230 where
231 F: FnOnce(&Script) -> R,
232 {
233 let content = self.text();
234 match Script::parse(&content) {
235 Ok(script) => Ok(f(&script)),
236 Err(e) => Err(EditorError::from(e)),
237 }
238 }
239
240 pub fn validate(&self) -> Result<()> {
243 let content = self.text();
244 Script::parse(&content).map_err(EditorError::from)?;
245 Ok(())
246 }
247
248 pub fn execute_command(
256 &mut self,
257 command: &dyn crate::commands::EditorCommand,
258 ) -> Result<crate::commands::CommandResult> {
259 use crate::core::history::Operation;
260
261 let _cursor_before = self.cursor_position();
263
264 let result = command.execute(self)?;
266
267 if result.content_changed {
269 let operation = if let Some(range) = result.modified_range {
271 if range.is_empty() {
272 let inserted_text = self.text_range(Range::new(
274 range.start,
275 Position::new(
276 range.start.offset
277 + result
278 .new_cursor
279 .map_or(0, |c| c.offset - range.start.offset),
280 ),
281 ))?;
282 Operation::Insert {
283 position: range.start,
284 text: inserted_text,
285 }
286 } else {
287 let cmd_desc = command.description();
291 if cmd_desc.contains("batch") || cmd_desc.contains("Multiple") {
292 Operation::Insert {
295 position: range.start,
296 text: String::new(),
297 }
298 } else {
299 Operation::Replace {
301 range,
302 old_text: String::new(), new_text: self.text_range(range).unwrap_or_default(),
304 }
305 }
306 }
307 } else {
308 Operation::Insert {
310 position: Position::new(0),
311 text: String::new(),
312 }
313 };
314
315 if let Some(new_cursor) = result.new_cursor {
317 self.set_cursor_position(Some(new_cursor));
318 }
319
320 self.history
322 .record_operation(operation, command.description().to_string(), &result);
323
324 self.validator.clear_cache();
326 }
327
328 Ok(result)
329 }
330
331 pub fn validate_comprehensive(&mut self) -> Result<crate::utils::validator::ValidationResult> {
336 let temp_doc = EditorDocument::from_content(&self.text())?;
339 let result = self.validator.validate(&temp_doc)?;
340 Ok(result.clone())
341 }
342
343 pub fn force_validate(&mut self) -> Result<crate::utils::validator::ValidationResult> {
345 let temp_doc = EditorDocument::from_content(&self.text())?;
347 let result = self.validator.force_validate(&temp_doc)?;
348 Ok(result.clone())
349 }
350
351 pub fn is_valid_cached(&mut self) -> Result<bool> {
353 let temp_doc = EditorDocument::from_content(&self.text())?;
355 self.validator.is_valid(&temp_doc)
356 }
357
358 pub fn validation_result(&self) -> Option<&crate::utils::validator::ValidationResult> {
360 self.validator.cached_result()
361 }
362
363 pub fn set_validator_config(&mut self, config: crate::utils::validator::ValidatorConfig) {
365 self.validator.set_config(config);
366 }
367
368 pub fn validator_mut(&mut self) -> &mut crate::utils::validator::LazyValidator {
370 &mut self.validator
371 }
372
373 #[must_use]
375 pub fn id(&self) -> &str {
376 &self.id
377 }
378
379 #[must_use]
381 pub fn file_path(&self) -> Option<&str> {
382 self.file_path.as_deref()
383 }
384
385 pub fn set_file_path(&mut self, path: Option<String>) {
387 self.file_path = path;
388 }
389
390 #[cfg(feature = "formats")]
392 pub fn import_format(
393 content: &str,
394 format: Option<crate::utils::formats::SubtitleFormat>,
395 ) -> Result<Self> {
396 let ass_content = crate::utils::formats::FormatConverter::import(content, format)?;
397 Self::from_content(&ass_content)
398 }
399
400 #[cfg(feature = "formats")]
402 pub fn export_format(
403 &self,
404 format: crate::utils::formats::SubtitleFormat,
405 options: &crate::utils::formats::ConversionOptions,
406 ) -> Result<String> {
407 crate::utils::formats::FormatConverter::export(self, format, options)
408 }
409
410 #[must_use]
412 pub const fn is_modified(&self) -> bool {
413 self.modified
414 }
415
416 #[must_use]
418 pub fn cursor_position(&self) -> Option<Position> {
419 self.history.cursor_position()
420 }
421
422 pub fn set_cursor_position(&mut self, position: Option<Position>) {
424 self.history.set_cursor(position);
425 }
426
427 pub fn set_modified(&mut self, modified: bool) {
429 self.modified = modified;
430 }
431
432 #[must_use]
434 pub fn len_bytes(&self) -> usize {
435 #[cfg(feature = "rope")]
436 {
437 self.text_rope.len_bytes()
438 }
439 #[cfg(not(feature = "rope"))]
440 {
441 self.text_content.len()
442 }
443 }
444
445 #[must_use]
447 pub fn len_lines(&self) -> usize {
448 #[cfg(feature = "rope")]
449 {
450 self.text_rope.len_lines()
451 }
452 #[cfg(not(feature = "rope"))]
453 {
454 self.text_content.lines().count().max(1)
455 }
456 }
457
458 #[must_use]
460 pub fn is_empty(&self) -> bool {
461 self.len_bytes() == 0
462 }
463
464 #[must_use]
466 pub fn text(&self) -> String {
467 #[cfg(feature = "rope")]
468 {
469 self.text_rope.to_string()
470 }
471 #[cfg(not(feature = "rope"))]
472 {
473 self.text_content.clone()
474 }
475 }
476
477 #[cfg(feature = "rope")]
479 #[must_use]
480 pub fn rope(&self) -> &ropey::Rope {
481 &self.text_rope
482 }
483
484 #[must_use]
486 pub fn len(&self) -> usize {
487 #[cfg(feature = "rope")]
488 {
489 self.text_rope.len_bytes()
490 }
491 #[cfg(not(feature = "rope"))]
492 {
493 self.text_content.len()
494 }
495 }
496
497 pub fn text_range(&self, range: Range) -> Result<String> {
499 let start = range.start.offset;
500 let end = range.end.offset;
501
502 if end > self.len_bytes() {
503 return Err(EditorError::InvalidRange {
504 start,
505 end,
506 length: self.len_bytes(),
507 });
508 }
509
510 #[cfg(feature = "rope")]
511 {
512 let start_char = self.text_rope.byte_to_char(start);
514 let end_char = self.text_rope.byte_to_char(end);
515 Ok(self.text_rope.slice(start_char..end_char).to_string())
516 }
517 #[cfg(not(feature = "rope"))]
518 {
519 Ok(self.text_content[start..end].to_string())
520 }
521 }
522
523 #[cfg(feature = "rope")]
525 pub fn position_to_line_column(&self, pos: Position) -> Result<LineColumn> {
526 if pos.offset > self.len_bytes() {
527 return Err(EditorError::PositionOutOfBounds {
528 position: pos.offset,
529 length: self.len_bytes(),
530 });
531 }
532
533 let line_idx = self.text_rope.byte_to_line(pos.offset);
534 let line_start = self.text_rope.line_to_byte(line_idx);
535 let col_offset = pos.offset - line_start;
536
537 let line = self.text_rope.line(line_idx);
539 let mut char_col = 0;
540 let mut byte_count = 0;
541
542 for ch in line.chars() {
543 if byte_count >= col_offset {
544 break;
545 }
546 byte_count += ch.len_utf8();
547 char_col += 1;
548 }
549
550 LineColumn::new(line_idx + 1, char_col + 1)
552 }
553
554 #[cfg(not(feature = "rope"))]
556 pub fn position_to_line_column(&self, pos: Position) -> Result<LineColumn> {
557 if pos.offset > self.len_bytes() {
558 return Err(EditorError::PositionOutOfBounds {
559 position: pos.offset,
560 length: self.len_bytes(),
561 });
562 }
563
564 let mut line = 1;
565 let mut col = 1;
566 let mut byte_pos = 0;
567
568 for ch in self.text_content.chars() {
569 if byte_pos >= pos.offset {
570 break;
571 }
572
573 if ch == '\n' {
574 line += 1;
575 col = 1;
576 } else {
577 col += 1;
578 }
579
580 byte_pos += ch.len_utf8();
581 }
582
583 LineColumn::new(line, col)
584 }
585
586 pub(crate) fn insert_raw(&mut self, pos: Position, text: &str) -> Result<()> {
588 if pos.offset > self.len_bytes() {
589 return Err(EditorError::PositionOutOfBounds {
590 position: pos.offset,
591 length: self.len_bytes(),
592 });
593 }
594
595 #[cfg(feature = "rope")]
596 {
597 let char_idx = self.text_rope.byte_to_char(pos.offset);
599 self.text_rope.insert(char_idx, text);
600 }
601 #[cfg(not(feature = "rope"))]
602 {
603 self.text_content.insert_str(pos.offset, text);
604 }
605
606 self.modified = true;
607 Ok(())
608 }
609
610 pub fn insert(&mut self, pos: Position, text: &str) -> Result<()> {
635 use crate::commands::{EditorCommand, InsertTextCommand};
636 use crate::core::history::Operation;
637
638 let command = InsertTextCommand::new(pos, text.to_string());
639 let result = command.execute(self)?;
640
641 let operation = Operation::Insert {
643 position: pos,
644 text: text.to_string(),
645 };
646 self.history
647 .record_operation(operation, command.description().to_string(), &result);
648
649 self.validator.clear_cache();
651
652 #[cfg(feature = "std")]
654 self.emit(DocumentEvent::TextInserted {
655 position: pos,
656 text: text.to_string(),
657 length: text.len(),
658 });
659
660 Ok(())
661 }
662
663 pub fn delete(&mut self, range: Range) -> Result<()> {
665 use crate::commands::{DeleteTextCommand, EditorCommand};
666 use crate::core::history::Operation;
667
668 let deleted_text = self.text_range(range)?;
670
671 let command = DeleteTextCommand::new(range);
672 let result = command.execute(self)?;
673
674 let operation = Operation::Delete {
676 range,
677 deleted_text: deleted_text.clone(),
678 };
679 self.history
680 .record_operation(operation, command.description().to_string(), &result);
681
682 self.validator.clear_cache();
684
685 #[cfg(feature = "std")]
687 self.emit(DocumentEvent::TextDeleted {
688 range,
689 deleted_text,
690 });
691
692 Ok(())
693 }
694
695 pub fn replace(&mut self, range: Range, text: &str) -> Result<()> {
697 use crate::commands::{EditorCommand, ReplaceTextCommand};
698 use crate::core::history::Operation;
699
700 let old_text = self.text_range(range)?;
702
703 let command = ReplaceTextCommand::new(range, text.to_string());
704 let result = command.execute(self)?;
705
706 let operation = Operation::Replace {
708 range,
709 old_text: old_text.clone(),
710 new_text: text.to_string(),
711 };
712 self.history
713 .record_operation(operation, command.description().to_string(), &result);
714
715 self.validator.clear_cache();
717
718 #[cfg(feature = "std")]
720 self.emit(DocumentEvent::TextReplaced {
721 range,
722 old_text,
723 new_text: text.to_string(),
724 });
725
726 Ok(())
727 }
728
729 fn generate_id() -> String {
731 #[cfg(feature = "std")]
733 {
734 use std::time::{SystemTime, UNIX_EPOCH};
735 let timestamp = SystemTime::now()
736 .duration_since(UNIX_EPOCH)
737 .unwrap_or_default()
738 .as_nanos();
739 format!("doc_{timestamp}")
740 }
741 #[cfg(not(feature = "std"))]
742 {
743 static mut COUNTER: u32 = 0;
744 #[allow(static_mut_refs)]
745 unsafe {
746 COUNTER += 1;
747 format!("doc_{COUNTER}")
748 }
749 }
750 }
751
752 pub fn events_count(&self) -> Result<usize> {
756 self.parse_script_with(|script| {
757 let mut count = 0;
758 for section in script.sections() {
759 if let Section::Events(events) = section {
760 count += events.len();
761 }
762 }
763 count
764 })
765 }
766
767 pub fn styles_count(&self) -> Result<usize> {
769 self.parse_script_with(|script| {
770 let mut count = 0;
771 for section in script.sections() {
772 if let Section::Styles(styles) = section {
773 count += styles.len();
774 }
775 }
776 count
777 })
778 }
779
780 pub fn script_info_fields(&self) -> Result<Vec<String>> {
782 self.parse_script_with(|script| {
783 let mut fields = Vec::new();
784 for section in script.sections() {
785 if let Section::ScriptInfo(info) = section {
786 for (key, _) in &info.fields {
787 fields.push(key.to_string());
788 }
789 }
790 }
791 fields
792 })
793 }
794
795 pub fn sections_count(&self) -> Result<usize> {
797 self.parse_script_with(|script| script.sections().len())
798 }
799
800 pub fn has_events(&self) -> Result<bool> {
802 self.parse_script_with(|script| {
803 script
804 .sections()
805 .iter()
806 .any(|section| matches!(section, Section::Events(_)))
807 })
808 }
809
810 pub fn has_styles(&self) -> Result<bool> {
812 self.parse_script_with(|script| {
813 script
814 .sections()
815 .iter()
816 .any(|section| matches!(section, Section::Styles(_)))
817 })
818 }
819
820 pub fn find_event_text(&self, pattern: &str) -> Result<Vec<String>> {
822 self.parse_script_with(|script| {
823 let mut matches = Vec::new();
824 for section in script.sections() {
825 if let Section::Events(events) = section {
826 for event in events {
827 if event.text.contains(pattern) {
828 matches.push(event.text.to_string());
829 }
830 }
831 }
832 }
833 matches
834 })
835 }
836
837 pub fn edit_event_by_index<F>(&mut self, index: usize, update_fn: F) -> Result<String>
867 where
868 F: for<'a> FnOnce(&ass_core::parser::ast::Event<'a>) -> Vec<(&'static str, String)>,
869 {
870 let content = self.text();
871 let mut event_info = None;
872 let mut event_count = 0;
873
874 self.parse_script_with(|script| -> Result<()> {
876 for section in script.sections() {
877 if let Section::Events(events) = section {
878 for event in events {
879 if event_count == index {
880 let modifications = update_fn(event);
882
883 let event_type_str = event.event_type.as_str();
885 let pattern = format!(
886 "{}: {},{},{}",
887 event_type_str, event.layer, event.start, event.end
888 );
889
890 let event_line = if let Some(pos) = content.find(&pattern) {
891 let line_end = content[pos..]
892 .find('\n')
893 .map(|n| pos + n)
894 .unwrap_or(content.len());
895 let line = content[pos..line_end].to_string();
896 (pos, line_end, line)
897 } else {
898 return Err(EditorError::ValidationError {
899 message: "Could not find event line in document".to_string(),
900 });
901 };
902
903 let event_data = (
905 event.event_type,
906 event.layer.to_string(),
907 event.start.to_string(),
908 event.end.to_string(),
909 event.style.to_string(),
910 event.name.to_string(),
911 event.margin_l.to_string(),
912 event.margin_r.to_string(),
913 event.margin_v.to_string(),
914 event.effect.to_string(),
915 event.text.to_string(),
916 );
917 event_info = Some((event_data, event_line, modifications));
918 return Ok(());
919 }
920 event_count += 1;
921 }
922 }
923 }
924 Ok(())
925 })??;
926
927 if let Some((event_data, (line_start, line_end, original_line), modifications)) = event_info
928 {
929 let new_line = self.build_modified_event_line_from_data(
931 event_data,
932 &original_line,
933 modifications,
934 )?;
935
936 let range = Range::new(Position::new(line_start), Position::new(line_end));
938 self.replace(range, &new_line)?;
939
940 Ok(new_line)
941 } else {
942 Err(EditorError::InvalidRange {
943 start: index,
944 end: index + 1,
945 length: event_count,
946 })
947 }
948 }
949
950 fn build_modified_event_line_from_data(
952 &self,
953 event_data: (
954 ass_core::parser::ast::EventType,
955 String,
956 String,
957 String,
958 String,
959 String,
960 String,
961 String,
962 String,
963 String,
964 String,
965 ),
966 _original_line: &str,
967 modifications: Vec<(&'static str, String)>,
968 ) -> Result<String> {
969 let (
970 event_type,
971 layer,
972 start,
973 end,
974 style,
975 name,
976 margin_l,
977 margin_r,
978 margin_v,
979 effect,
980 text,
981 ) = event_data;
982
983 let mut layer = layer;
985 let mut start = start;
986 let mut end = end;
987 let mut style = style;
988 let mut name = name;
989 let mut margin_l = margin_l;
990 let mut margin_r = margin_r;
991 let mut margin_v = margin_v;
992 let mut effect = effect;
993 let mut text = text;
994
995 for (field, value) in modifications {
996 match field {
997 "layer" => layer = value,
998 "start" => start = value,
999 "end" => end = value,
1000 "style" => style = value,
1001 "name" => name = value,
1002 "margin_l" => margin_l = value,
1003 "margin_r" => margin_r = value,
1004 "margin_v" => margin_v = value,
1005 "effect" => effect = value,
1006 "text" => text = value,
1007 _ => {
1008 return Err(EditorError::ValidationError {
1009 message: format!("Unknown event field: {field}"),
1010 });
1011 }
1012 }
1013 }
1014
1015 let event_type_str = event_type.as_str();
1017 Ok(format!("{event_type_str}: {layer},{start},{end},{style},{name},{margin_l},{margin_r},{margin_v},{effect},{text}"))
1018 }
1019
1020 pub fn add_event_line(&mut self, event_line: &str) -> Result<()> {
1022 let content = self.text();
1023 if let Some(events_pos) = content.find("[Events]") {
1024 let format_start = content[events_pos..].find("Format:").unwrap_or(0) + events_pos;
1026 let line_end = content[format_start..].find('\n').unwrap_or(0) + format_start + 1;
1027
1028 let insert_pos = Position::new(line_end);
1029 self.insert(insert_pos, &format!("{event_line}\n"))
1030 } else {
1031 let content_len = self.len_bytes();
1033 let events_section = format!("\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n{event_line}\n");
1034 self.insert(Position::new(content_len), &events_section)
1035 }
1036 }
1037
1038 pub fn edit_style_line(&mut self, style_name: &str, new_style_line: &str) -> Result<()> {
1040 let content = self.text();
1041 let pattern = format!("Style: {style_name},");
1042
1043 if let Some(pos) = content.find(&pattern) {
1044 let line_end = content[pos..].find('\n').map_or(content.len(), |n| pos + n);
1046 let range = Range::new(Position::new(pos), Position::new(line_end));
1047 self.replace(range, new_style_line)
1048 } else {
1049 self.add_style_line(new_style_line)
1051 }
1052 }
1053
1054 pub fn add_style_line(&mut self, style_line: &str) -> Result<()> {
1056 let content = self.text();
1057 if let Some(styles_pos) = content
1058 .find("[V4+ Styles]")
1059 .or_else(|| content.find("[V4 Styles]"))
1060 {
1061 let format_start = content[styles_pos..].find("Format:").unwrap_or(0) + styles_pos;
1063 let line_end = content[format_start..].find('\n').unwrap_or(0) + format_start + 1;
1064
1065 let insert_pos = Position::new(line_end);
1066 self.insert(insert_pos, &format!("{style_line}\n"))
1067 } else {
1068 let script_info_end = content.find("\n[Events]").unwrap_or(content.len());
1070 let styles_section = format!("\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n{style_line}\n");
1071 self.insert(Position::new(script_info_end), &styles_section)
1072 }
1073 }
1074
1075 #[cfg(feature = "stream")]
1084 pub fn edit_incremental(&mut self, range: Range, new_text: &str) -> Result<ScriptDeltaOwned> {
1085 use crate::core::history::Operation;
1086
1087 let is_simple_edit = new_text.len() <= 100 && !new_text.contains('[') && new_text.matches('\n').count() <= 1 && range.len() <= 50; if is_simple_edit {
1094 return self.edit_fast_path(range, new_text);
1095 }
1096
1097 #[cfg(feature = "std")]
1099 let old_text = self.text_range(range)?;
1100 #[cfg(not(feature = "std"))]
1101 let _old_text = self.text_range(range)?;
1102
1103 let current_text = self.text();
1105 let delta = match self
1106 .incremental_parser
1107 .apply_change(¤t_text, range, new_text)
1108 {
1109 Ok(delta) => delta,
1110 Err(_e) => {
1111 #[cfg(feature = "std")]
1113 eprintln!("Incremental parsing failed, attempting recovery: {_e}");
1114
1115 if self.incremental_parser.should_reparse() {
1117 self.incremental_parser.clear_cache();
1118 }
1119
1120 match self
1122 .incremental_parser
1123 .apply_change(¤t_text, range, new_text)
1124 {
1125 Ok(delta) => delta,
1126 Err(_) => {
1127 ScriptDeltaOwned {
1129 added: Vec::new(),
1130 modified: vec![(0, "Script modified".to_string())],
1131 removed: Vec::new(),
1132 new_issues: Vec::new(),
1133 }
1134 }
1135 }
1136 }
1137 };
1138
1139 let undo_data = self.capture_delta_undo_data(&delta)?;
1141
1142 let operation = Operation::Delta {
1144 forward: delta.clone(),
1145 undo_data,
1146 };
1147
1148 let result = CommandResult::success_with_change(
1150 range,
1151 Position::new(range.start.offset + new_text.len()),
1152 );
1153
1154 let result_with_delta = result.with_delta(delta.clone());
1156 self.history.record_operation(
1157 operation,
1158 format!("Incremental edit at {}", range.start.offset),
1159 &result_with_delta,
1160 );
1161
1162 self.replace_raw(range, new_text)?;
1164
1165 self.modified = true;
1167
1168 #[cfg(feature = "std")]
1170 self.emit(DocumentEvent::TextReplaced {
1171 range,
1172 old_text,
1173 new_text: new_text.to_string(),
1174 });
1175
1176 Ok(delta)
1177 }
1178
1179 #[cfg(feature = "stream")]
1181 pub fn insert_incremental(&mut self, pos: Position, text: &str) -> Result<ScriptDeltaOwned> {
1182 let range = Range::new(pos, pos); self.edit_incremental(range, text)
1184 }
1185
1186 #[cfg(feature = "stream")]
1188 pub fn delete_incremental(&mut self, range: Range) -> Result<ScriptDeltaOwned> {
1189 self.edit_incremental(range, "")
1190 }
1191
1192 #[cfg(feature = "stream")]
1194 fn edit_fast_path(&mut self, range: Range, new_text: &str) -> Result<ScriptDeltaOwned> {
1195 use crate::core::history::Operation;
1196
1197 let text = self.text();
1199 if !text.is_char_boundary(range.start.offset) || !text.is_char_boundary(range.end.offset) {
1200 return self.edit_incremental_fallback(range, new_text);
1202 }
1203
1204 let old_text = if range.is_empty() {
1206 String::new()
1207 } else {
1208 self.text_range(range)?
1209 };
1210
1211 self.replace_raw(range, new_text)?;
1213
1214 let operation = Operation::Replace {
1216 range,
1217 new_text: new_text.to_string(),
1218 old_text,
1219 };
1220
1221 let result = CommandResult::success_with_change(
1223 range,
1224 Position::new(range.start.offset + new_text.len()),
1225 );
1226
1227 self.history
1229 .record_operation(operation, "Fast character insert".to_string(), &result);
1230
1231 self.modified = true;
1233
1234 Ok(ScriptDeltaOwned {
1236 added: Vec::new(),
1237 modified: Vec::new(),
1238 removed: Vec::new(),
1239 new_issues: Vec::new(),
1240 })
1241 }
1242
1243 #[cfg(feature = "stream")]
1245 fn edit_incremental_fallback(
1246 &mut self,
1247 range: Range,
1248 new_text: &str,
1249 ) -> Result<ScriptDeltaOwned> {
1250 self.replace(range, new_text)?;
1252
1253 Ok(ScriptDeltaOwned {
1255 added: Vec::new(),
1256 modified: Vec::new(),
1257 removed: Vec::new(),
1258 new_issues: Vec::new(),
1259 })
1260 }
1261
1262 pub fn edit_safe(&mut self, range: Range, new_text: &str) -> Result<()> {
1268 #[cfg(feature = "stream")]
1269 {
1270 match self.edit_incremental(range, new_text) {
1272 Ok(_) => return Ok(()),
1273 Err(_e) => {
1274 #[cfg(feature = "std")]
1275 eprintln!("Incremental edit failed, falling back to regular replace: {_e}");
1276 }
1277 }
1278 }
1279
1280 self.replace(range, new_text)
1282 }
1283
1284 #[cfg(feature = "stream")]
1286 pub fn edit_event_incremental(
1287 &mut self,
1288 event_text: &str,
1289 new_text: &str,
1290 ) -> Result<ScriptDeltaOwned> {
1291 let content = self.text();
1292 if let Some(pos) = content.find(event_text) {
1293 let range = Range::new(Position::new(pos), Position::new(pos + event_text.len()));
1294 self.edit_incremental(range, new_text)
1295 } else {
1296 Err(EditorError::ValidationError {
1297 message: format!("Event text not found: {event_text}"),
1298 })
1299 }
1300 }
1301
1302 #[cfg(feature = "stream")]
1304 pub fn parse_with_delta_tracking<F, R>(
1305 &self,
1306 range: Option<StdRange<usize>>,
1307 new_text: Option<&str>,
1308 f: F,
1309 ) -> Result<R>
1310 where
1311 F: FnOnce(&Script, Option<&ScriptDeltaOwned>) -> R,
1312 {
1313 let content = self.text();
1314 let script = Script::parse(&content).map_err(EditorError::from)?;
1315
1316 if let (Some(range), Some(text)) = (range, new_text) {
1317 match script.parse_partial(range, text) {
1319 Ok(delta) => Ok(f(&script, Some(&delta))),
1320 Err(_) => {
1321 Ok(f(&script, None))
1323 }
1324 }
1325 } else {
1326 Ok(f(&script, None))
1327 }
1328 }
1329
1330 pub fn edit_event_with_builder<F>(&mut self, index: usize, builder_fn: F) -> Result<String>
1362 where
1363 F: for<'a> FnOnce(
1364 crate::core::builders::EventBuilder,
1365 ) -> crate::core::builders::EventBuilder,
1366 {
1367 use crate::core::builders::EventBuilder;
1368
1369 let content = self.text();
1370 let mut event_info = None;
1371 let mut event_count = 0;
1372 let mut format_line = None;
1373
1374 self.parse_script_with(|script| -> Result<()> {
1376 for section in script.sections() {
1377 if let Section::Events(events) = section {
1378 if format_line.is_none() {
1380 if let Some(events_pos) = content.find("[Events]") {
1382 let after_header = &content[events_pos + 8..];
1383 if let Some(format_pos) = after_header.find("Format:") {
1384 let format_start = events_pos + 8 + format_pos + 7; if let Some(format_end) = content[format_start..].find('\n') {
1386 let format_str =
1387 content[format_start..format_start + format_end].trim();
1388 let fields: Vec<&str> =
1389 format_str.split(',').map(str::trim).collect();
1390 format_line = Some(fields);
1391 }
1392 }
1393 }
1394 }
1395
1396 for event in events {
1397 if event_count == index {
1398 let mut builder = match event.event_type {
1400 ass_core::parser::ast::EventType::Dialogue => {
1401 EventBuilder::dialogue()
1402 }
1403 ass_core::parser::ast::EventType::Comment => {
1404 EventBuilder::comment()
1405 }
1406 _ => EventBuilder::new(),
1407 };
1408
1409 builder = builder
1411 .layer(event.layer.parse::<u32>().unwrap_or(0))
1412 .start_time(event.start)
1413 .end_time(event.end)
1414 .style(event.style)
1415 .speaker(event.name)
1416 .margin_left(event.margin_l.parse::<u32>().unwrap_or(0))
1417 .margin_right(event.margin_r.parse::<u32>().unwrap_or(0))
1418 .margin_vertical(event.margin_v.parse::<u32>().unwrap_or(0))
1419 .effect(event.effect)
1420 .text(event.text);
1421
1422 if let Some(margin_t) = event.margin_t {
1423 builder = builder.margin_top(margin_t.parse::<u32>().unwrap_or(0));
1424 }
1425 if let Some(margin_b) = event.margin_b {
1426 builder =
1427 builder.margin_bottom(margin_b.parse::<u32>().unwrap_or(0));
1428 }
1429
1430 let modified_builder = builder_fn(builder);
1432
1433 let new_line = if let Some(ref format_fields) = format_line {
1435 modified_builder.build_with_format(format_fields)?
1436 } else {
1437 modified_builder.build()?
1438 };
1439
1440 let event_type_str = event.event_type.as_str();
1442 let pattern = format!(
1443 "{}: {},{},{}",
1444 event_type_str, event.layer, event.start, event.end
1445 );
1446
1447 let event_line = if let Some(pos) = content.find(&pattern) {
1448 let line_end = content[pos..]
1449 .find('\n')
1450 .map(|n| pos + n)
1451 .unwrap_or(content.len());
1452 (pos, line_end)
1453 } else {
1454 return Err(EditorError::ValidationError {
1455 message: "Could not find event line in document".to_string(),
1456 });
1457 };
1458
1459 event_info = Some((event_line, new_line));
1460 return Ok(());
1461 }
1462 event_count += 1;
1463 }
1464 }
1465 }
1466 Ok(())
1467 })??;
1468
1469 if let Some(((line_start, line_end), new_line)) = event_info {
1470 let range = Range::new(Position::new(line_start), Position::new(line_end));
1472 self.replace(range, &new_line)?;
1473
1474 Ok(new_line)
1475 } else {
1476 Err(EditorError::InvalidRange {
1477 start: index,
1478 end: index + 1,
1479 length: event_count,
1480 })
1481 }
1482 }
1483
1484 pub fn edit_event_text(&mut self, old_text: &str, new_text: &str) -> Result<()> {
1486 let content = self.text();
1487
1488 if let Some(pos) = content.find(old_text) {
1489 let range = Range::new(Position::new(pos), Position::new(pos + old_text.len()));
1490 self.replace(range, new_text)?;
1491 }
1492
1493 Ok(())
1494 }
1495
1496 pub fn get_script_info_field(&self, key: &str) -> Result<Option<String>> {
1498 self.parse_script_with(|script| {
1499 script.sections().iter().find_map(|section| {
1500 if let Section::ScriptInfo(info) = section {
1501 info.fields
1502 .iter()
1503 .find(|(k, _)| *k == key)
1504 .map(|(_, v)| v.to_string())
1505 } else {
1506 None
1507 }
1508 })
1509 })
1510 }
1511
1512 pub fn set_script_info_field(&mut self, key: &str, value: &str) -> Result<()> {
1514 let content = self.text();
1516 let field_pattern = format!("{key}:");
1517
1518 if let Some(pos) = content.find(&field_pattern) {
1519 let line_start = pos;
1521 let line_end = content[pos..].find('\n').map_or(content.len(), |n| pos + n);
1522
1523 let range = Range::new(Position::new(line_start), Position::new(line_end));
1524
1525 let new_line = format!("{key}: {value}");
1526 self.replace(range, &new_line)?;
1527 }
1528
1529 Ok(())
1530 }
1531
1532 pub fn undo(&mut self) -> Result<CommandResult> {
1537 use crate::core::history::Operation;
1538
1539 if let Some(entry) = self.history.pop_undo_entry() {
1541 let mut result = CommandResult::success();
1542 result.content_changed = true;
1543
1544 match &entry.operation {
1546 Operation::Insert { position, text } => {
1547 let end_pos = Position::new(position.offset + text.len());
1549 let range = Range::new(*position, end_pos);
1550 self.delete_raw(range)?;
1551 result.modified_range = Some(Range::new(*position, *position));
1552 result.new_cursor = entry.cursor_before;
1553 }
1554 Operation::Delete {
1555 range,
1556 deleted_text,
1557 } => {
1558 self.insert_raw(range.start, deleted_text)?;
1560 let end_pos = Position::new(range.start.offset + deleted_text.len());
1561 result.modified_range = Some(Range::new(range.start, end_pos));
1562 result.new_cursor = entry.cursor_before;
1563 }
1564 Operation::Replace {
1565 range, old_text, ..
1566 } => {
1567 self.replace_raw(*range, old_text)?;
1569 let end_pos = Position::new(range.start.offset + old_text.len());
1570 result.modified_range = Some(Range::new(range.start, end_pos));
1571 result.new_cursor = entry.cursor_before;
1572 }
1573 #[cfg(feature = "stream")]
1574 Operation::Delta { forward, undo_data } => {
1575 for (index, section_text) in undo_data.removed_sections.iter() {
1577 self.insert_section_at(*index, section_text)?;
1578 }
1579
1580 for (index, original_text) in undo_data.modified_sections.iter() {
1582 self.replace_section(*index, original_text)?;
1583 }
1584
1585 for _ in 0..forward.added.len() {
1587 self.remove_last_section()?;
1588 }
1589
1590 result.message = Some("Delta operation undone".to_string());
1591 }
1592 }
1593
1594 self.history.push_redo_entry(entry);
1596
1597 #[cfg(feature = "stream")]
1599 if let Some(delta) = result.script_delta.as_ref() {
1600 self.apply_script_delta(delta.clone())?;
1601 }
1602
1603 result.message = Some("Undo successful".to_string());
1604 Ok(result)
1605 } else {
1606 Err(EditorError::NothingToUndo)
1607 }
1608 }
1609
1610 pub fn redo(&mut self) -> Result<CommandResult> {
1615 use crate::core::history::Operation;
1616
1617 if let Some(entry) = self.history.pop_redo_entry() {
1619 let mut result = CommandResult::success();
1620 result.content_changed = true;
1621
1622 match &entry.operation {
1624 Operation::Insert { position, text } => {
1625 self.insert_raw(*position, text)?;
1627 let end_pos = Position::new(position.offset + text.len());
1628 result.modified_range = Some(Range::new(*position, end_pos));
1629 result.new_cursor = entry.cursor_after;
1630 }
1631 Operation::Delete { range, .. } => {
1632 self.delete_raw(*range)?;
1634 result.modified_range = Some(Range::new(range.start, range.start));
1635 result.new_cursor = entry.cursor_after;
1636 }
1637 Operation::Replace {
1638 range, new_text, ..
1639 } => {
1640 self.replace_raw(*range, new_text)?;
1642 let end_pos = Position::new(range.start.offset + new_text.len());
1643 result.modified_range = Some(Range::new(range.start, end_pos));
1644 result.new_cursor = entry.cursor_after;
1645 }
1646 #[cfg(feature = "stream")]
1647 Operation::Delta {
1648 forward,
1649 undo_data: _,
1650 } => {
1651 self.apply_script_delta(forward.clone())?;
1653 result.message = Some("Delta re-applied".to_string());
1654 }
1655 }
1656
1657 if let Some(cursor) = result.new_cursor {
1660 self.history.set_cursor(Some(cursor));
1661 }
1662
1663 let new_entry = crate::core::history::HistoryEntry::new(
1665 entry.operation,
1666 entry.description,
1667 &result,
1668 entry.cursor_before,
1669 );
1670
1671 self.history.stack_mut().push(new_entry);
1673
1674 result.message = Some("Redo successful".to_string());
1675 Ok(result)
1676 } else {
1677 Err(EditorError::NothingToRedo)
1678 }
1679 }
1680
1681 pub fn can_undo(&self) -> bool {
1683 self.history.can_undo()
1684 }
1685
1686 pub fn can_redo(&self) -> bool {
1688 self.history.can_redo()
1689 }
1690
1691 pub fn next_undo_description(&self) -> Option<&str> {
1693 self.history.next_undo_description()
1694 }
1695
1696 pub fn next_redo_description(&self) -> Option<&str> {
1698 self.history.next_redo_description()
1699 }
1700
1701 pub fn undo_manager_mut(&mut self) -> &mut UndoManager {
1703 &mut self.history
1704 }
1705
1706 pub fn undo_manager(&self) -> &UndoManager {
1708 &self.history
1709 }
1710
1711 #[cfg(feature = "stream")]
1713 pub fn apply_script_delta(&mut self, delta: ScriptDeltaOwned) -> Result<()> {
1714 use crate::core::history::Operation;
1715
1716 let undo_data = self.capture_delta_undo_data(&delta)?;
1718
1719 self.apply_script_delta_internal(delta.clone())?;
1721
1722 let operation = Operation::Delta {
1724 forward: delta,
1725 undo_data,
1726 };
1727
1728 let result = CommandResult::success();
1729 self.history
1730 .record_operation(operation, "Apply delta".to_string(), &result);
1731
1732 Ok(())
1733 }
1734
1735 #[cfg(feature = "stream")]
1737 fn capture_delta_undo_data(
1738 &self,
1739 delta: &ScriptDeltaOwned,
1740 ) -> Result<crate::core::history::DeltaUndoData> {
1741 let mut removed_sections = Vec::new();
1742 let mut modified_sections = Vec::new();
1743
1744 let content = self.text();
1746
1747 #[cfg(feature = "stream")]
1750 let is_small_edit = delta.added.len() + delta.removed.len() + delta.modified.len() <= 2;
1751 #[cfg(not(feature = "stream"))]
1752 let is_small_edit = false;
1753
1754 if is_small_edit {
1755 } else {
1758 let _ = self.parse_script_with(|script| {
1760 for &index in &delta.removed {
1762 if let Some(section) = script.sections().get(index) {
1763 match self.extract_section_text(&content, section) {
1764 Ok(section_text) => removed_sections.push((index, section_text)),
1765 Err(_) => {
1766 removed_sections.push((index, String::new()));
1768 }
1769 }
1770 }
1771 }
1772
1773 for (index, _) in &delta.modified {
1775 if let Some(section) = script.sections().get(*index) {
1776 match self.extract_section_text(&content, section) {
1777 Ok(section_text) => modified_sections.push((*index, section_text)),
1778 Err(_) => {
1779 modified_sections.push((*index, String::new()));
1781 }
1782 }
1783 }
1784 }
1785
1786 Ok::<(), EditorError>(())
1787 })?;
1788 }
1789
1790 Ok(crate::core::history::DeltaUndoData {
1791 removed_sections,
1792 modified_sections,
1793 })
1794 }
1795
1796 #[cfg(feature = "stream")]
1798 fn extract_section_text(&self, content: &str, section: &Section) -> Result<String> {
1799 let header = match section {
1800 Section::ScriptInfo(_) => "[Script Info]",
1801 Section::Styles(_) => "[V4+ Styles]",
1802 Section::Events(_) => "[Events]",
1803 Section::Fonts(_) => "[Fonts]",
1804 Section::Graphics(_) => "[Graphics]",
1805 };
1806
1807 let start = content
1809 .find(header)
1810 .ok_or_else(|| EditorError::SectionNotFound {
1811 section: header.to_string(),
1812 })?;
1813
1814 let section_headers = [
1816 "[Script Info]",
1817 "[V4+ Styles]",
1818 "[Events]",
1819 "[Fonts]",
1820 "[Graphics]",
1821 ];
1822
1823 let end = content[start + header.len()..]
1824 .find(|_c: char| {
1825 for sh in §ion_headers {
1826 if content[start + header.len()..].starts_with(sh) {
1827 return true;
1828 }
1829 }
1830 false
1831 })
1832 .map(|pos| start + header.len() + pos)
1833 .unwrap_or(content.len());
1834
1835 Ok(content[start..end].to_string())
1836 }
1837
1838 #[cfg(feature = "stream")]
1840 fn apply_script_delta_internal(&mut self, delta: ScriptDeltaOwned) -> Result<()> {
1841 let current_content = self.text();
1843 let script = Script::parse(¤t_content).map_err(EditorError::from)?;
1844
1845 let mut removed_indices = delta.removed.clone();
1847 removed_indices.sort_by(|a, b| b.cmp(a)); for index in removed_indices {
1850 if index < script.sections().len() {
1851 let section = &script.sections()[index];
1853 let start_offset = self.find_section_start(section)?;
1854 let end_offset = self.find_section_end(section)?;
1855
1856 self.delete_raw(Range::new(
1857 Position::new(start_offset),
1858 Position::new(end_offset),
1859 ))?;
1860 }
1861 }
1862
1863 for (index, new_section_text) in delta.modified {
1865 if index < script.sections().len() {
1866 let section = &script.sections()[index];
1868 let start_offset = self.find_section_start(section)?;
1869 let end_offset = self.find_section_end(section)?;
1870
1871 self.replace_raw(
1873 Range::new(Position::new(start_offset), Position::new(end_offset)),
1874 &new_section_text,
1875 )?;
1876 }
1877 }
1878
1879 for section_text in delta.added {
1881 let end_pos = Position::new(self.len_bytes());
1883
1884 if self.len_bytes() > 0 && !self.text().ends_with('\n') {
1886 self.insert_raw(end_pos, "\n")?;
1887 }
1888
1889 self.insert_raw(Position::new(self.len_bytes()), §ion_text)?;
1890
1891 if !section_text.ends_with('\n') {
1893 self.insert_raw(Position::new(self.len_bytes()), "\n")?;
1894 }
1895 }
1896
1897 let _ = Script::parse(&self.text()).map_err(EditorError::from)?;
1899
1900 Ok(())
1901 }
1902
1903 #[cfg(feature = "stream")]
1905 fn find_section_start(&self, section: &Section) -> Result<usize> {
1906 let header = match section {
1908 Section::ScriptInfo(_) => "[Script Info]",
1909 Section::Styles(_) => "[V4+ Styles]",
1910 Section::Events(_) => "[Events]",
1911 Section::Fonts(_) => "[Fonts]",
1912 Section::Graphics(_) => "[Graphics]",
1913 };
1914
1915 if let Some(pos) = self.text().find(header) {
1917 Ok(pos)
1918 } else {
1919 Err(EditorError::SectionNotFound {
1920 section: header.to_string(),
1921 })
1922 }
1923 }
1924
1925 #[cfg(feature = "stream")]
1927 fn find_section_end(&self, section: &Section) -> Result<usize> {
1928 let start = self.find_section_start(section)?;
1929 let content = &self.text()[start..];
1930
1931 let section_headers = [
1933 "[Script Info]",
1934 "[V4+ Styles]",
1935 "[Events]",
1936 "[Fonts]",
1937 "[Graphics]",
1938 ];
1939
1940 let mut end_offset = content.len();
1941 for header in §ion_headers {
1942 if let Some(pos) = content.find(header) {
1943 if pos > 0 {
1944 end_offset = end_offset.min(pos);
1945 }
1946 }
1947 }
1948
1949 Ok(start + end_offset)
1950 }
1951
1952 pub(crate) fn delete_raw(&mut self, range: Range) -> Result<()> {
1954 if range.end.offset > self.len_bytes() {
1955 return Err(EditorError::InvalidRange {
1956 start: range.start.offset,
1957 end: range.end.offset,
1958 length: self.len_bytes(),
1959 });
1960 }
1961
1962 #[cfg(feature = "rope")]
1963 {
1964 let start_char = self.text_rope.byte_to_char(range.start.offset);
1966 let end_char = self.text_rope.byte_to_char(range.end.offset);
1967 self.text_rope.remove(start_char..end_char);
1968 }
1969 #[cfg(not(feature = "rope"))]
1970 {
1971 self.text_content
1972 .drain(range.start.offset..range.end.offset);
1973 }
1974
1975 self.modified = true;
1976 Ok(())
1977 }
1978
1979 pub(crate) fn replace_raw(&mut self, range: Range, text: &str) -> Result<()> {
1981 self.delete_raw(range)?;
1982 self.insert_raw(range.start, text)?;
1983 Ok(())
1984 }
1985
1986 #[cfg(feature = "stream")]
1990 fn insert_section_at(&mut self, index: usize, section_text: &str) -> Result<()> {
1991 let section_count = self.parse_script_with(|script| script.sections().len())?;
1993
1994 if index >= section_count {
1996 let end_pos = Position::new(self.len_bytes());
1997
1998 if self.len_bytes() > 0 && !self.text().ends_with('\n') {
2000 self.insert_raw(end_pos, "\n")?;
2001 }
2002
2003 self.insert_raw(Position::new(self.len_bytes()), section_text)?;
2004
2005 if !section_text.ends_with('\n') {
2007 self.insert_raw(Position::new(self.len_bytes()), "\n")?;
2008 }
2009
2010 return Ok(());
2011 }
2012
2013 let content = self.text();
2015 let insert_pos = self.parse_script_with(|script| -> Result<usize> {
2016 if let Some(section) = script.sections().get(index) {
2017 let header = match section {
2019 Section::ScriptInfo(_) => "[Script Info]",
2020 Section::Styles(_) => "[V4+ Styles]",
2021 Section::Events(_) => "[Events]",
2022 Section::Fonts(_) => "[Fonts]",
2023 Section::Graphics(_) => "[Graphics]",
2024 };
2025
2026 if let Some(pos) = content.find(header) {
2027 Ok(pos)
2028 } else {
2029 Err(EditorError::SectionNotFound {
2030 section: header.to_string(),
2031 })
2032 }
2033 } else {
2034 Ok(content.len())
2036 }
2037 })??;
2038
2039 let mut text_to_insert = section_text.to_string();
2041
2042 if !text_to_insert.ends_with('\n') {
2044 text_to_insert.push('\n');
2045 }
2046
2047 if insert_pos < content.len() {
2049 text_to_insert.push('\n');
2050 }
2051
2052 self.insert_raw(Position::new(insert_pos), &text_to_insert)?;
2053
2054 Ok(())
2055 }
2056
2057 #[cfg(feature = "stream")]
2059 fn replace_section(&mut self, index: usize, new_text: &str) -> Result<()> {
2060 let content = self.text();
2063 let section_info: Result<Option<&str>> = self.parse_script_with(|script| {
2064 if let Some(section) = script.sections().get(index) {
2065 let header = match section {
2066 Section::ScriptInfo(_) => "[Script Info]",
2067 Section::Styles(_) => "[V4+ Styles]",
2068 Section::Events(_) => "[Events]",
2069 Section::Fonts(_) => "[Fonts]",
2070 Section::Graphics(_) => "[Graphics]",
2071 };
2072 Ok(Some(header))
2073 } else {
2074 Ok(None)
2075 }
2076 })?;
2077
2078 if let Some(header) = section_info? {
2079 let start = self.find_section_start_by_header(&content, header)?;
2080 let end = self.find_section_end_from_start(&content, start)?;
2081
2082 self.replace_raw(
2083 Range::new(Position::new(start), Position::new(end)),
2084 new_text,
2085 )?;
2086 }
2087
2088 Ok(())
2089 }
2090
2091 #[cfg(feature = "stream")]
2093 fn remove_last_section(&mut self) -> Result<()> {
2094 let content = self.text();
2097 let section_info: Result<Option<&str>> = self.parse_script_with(|script| {
2098 if let Some(section) = script.sections().last() {
2099 let header = match section {
2100 Section::ScriptInfo(_) => "[Script Info]",
2101 Section::Styles(_) => "[V4+ Styles]",
2102 Section::Events(_) => "[Events]",
2103 Section::Fonts(_) => "[Fonts]",
2104 Section::Graphics(_) => "[Graphics]",
2105 };
2106 Ok(Some(header))
2107 } else {
2108 Ok(None)
2109 }
2110 })?;
2111
2112 if let Some(header) = section_info? {
2113 let start = self.find_section_start_by_header(&content, header)?;
2114 let end = self.find_section_end_from_start(&content, start)?;
2115
2116 self.delete_raw(Range::new(Position::new(start), Position::new(end)))?;
2117 }
2118
2119 Ok(())
2120 }
2121
2122 #[cfg(feature = "stream")]
2124 fn find_section_start_by_header(&self, content: &str, header: &str) -> Result<usize> {
2125 content
2126 .find(header)
2127 .ok_or_else(|| EditorError::SectionNotFound {
2128 section: header.to_string(),
2129 })
2130 }
2131
2132 #[cfg(feature = "stream")]
2134 fn find_section_end_from_start(&self, content: &str, start: usize) -> Result<usize> {
2135 let section_headers = [
2136 "[Script Info]",
2137 "[V4+ Styles]",
2138 "[Events]",
2139 "[Fonts]",
2140 "[Graphics]",
2141 ];
2142
2143 let mut end = content.len();
2145 for header in §ion_headers {
2146 if let Some(pos) = content[start + 1..].find(header) {
2147 let actual_pos = start + 1 + pos;
2148 if actual_pos < end {
2149 end = actual_pos;
2150 }
2151 }
2152 }
2153
2154 Ok(end)
2155 }
2156}
2157
2158impl Default for EditorDocument {
2159 fn default() -> Self {
2160 Self::new()
2161 }
2162}
2163
2164pub struct DocumentPosition<'a> {
2166 document: &'a mut EditorDocument,
2167 position: Position,
2168}
2169
2170impl<'a> DocumentPosition<'a> {
2171 pub fn insert_text(self, text: &str) -> Result<()> {
2173 self.document.insert(self.position, text)
2174 }
2175
2176 pub fn delete_range(self, len: usize) -> Result<()> {
2178 let end_pos = Position::new(self.position.offset + len);
2179 let range = Range::new(self.position, end_pos);
2180 self.document.delete(range)
2181 }
2182
2183 pub fn replace_text(self, len: usize, new_text: &str) -> Result<()> {
2185 let end_pos = Position::new(self.position.offset + len);
2186 let range = Range::new(self.position, end_pos);
2187 self.document.replace(range, new_text)
2188 }
2189}
2190
2191impl EditorDocument {
2192 pub fn at(&mut self, pos: Position) -> DocumentPosition {
2194 DocumentPosition {
2195 document: self,
2196 position: pos,
2197 }
2198 }
2199
2200 #[cfg(feature = "plugins")]
2202 pub fn initialize_registry(&mut self) -> Result<()> {
2203 use crate::extensions::registry_integration::RegistryIntegration;
2204
2205 let mut integration = RegistryIntegration::new();
2206
2207 crate::extensions::builtin::register_builtin_extensions(&mut integration)?;
2209
2210 self.registry_integration = Some(Arc::new(integration));
2211 Ok(())
2212 }
2213
2214 #[cfg(feature = "plugins")]
2216 pub fn registry(&self) -> Option<&ass_core::plugin::ExtensionRegistry> {
2217 self.registry_integration
2218 .as_ref()
2219 .map(|integration| integration.registry())
2220 }
2221
2222 #[cfg(feature = "plugins")]
2227 pub fn parse_with_extensions<F, R>(&self, f: F) -> Result<R>
2228 where
2229 F: FnOnce(&ass_core::parser::Script) -> R,
2230 {
2231 let content = self.text();
2232
2233 if let Some(integration) = &self.registry_integration {
2234 let script = ass_core::parser::Script::builder()
2236 .with_registry(integration.registry())
2237 .parse(&content)
2238 .map_err(EditorError::Core)?;
2239 Ok(f(&script))
2240 } else {
2241 let script = ass_core::parser::Script::parse(&content).map_err(EditorError::Core)?;
2243 Ok(f(&script))
2244 }
2245 }
2246
2247 #[cfg(feature = "plugins")]
2249 pub fn register_tag_handler(
2250 &mut self,
2251 extension_name: String,
2252 handler: Box<dyn ass_core::plugin::TagHandler>,
2253 ) -> Result<()> {
2254 if self.registry_integration.is_none() {
2255 self.initialize_registry()?;
2256 }
2257
2258 let registry_ref =
2259 self.registry_integration
2260 .as_mut()
2261 .ok_or_else(|| EditorError::ExtensionError {
2262 extension: extension_name.clone(),
2263 message: "Registry integration not available".to_string(),
2264 })?;
2265
2266 if let Some(integration) = Arc::get_mut(registry_ref) {
2267 integration.register_custom_tag_handler(extension_name, handler)
2268 } else {
2269 Err(EditorError::ExtensionError {
2270 extension: extension_name,
2271 message: "Cannot modify shared registry integration".to_string(),
2272 })
2273 }
2274 }
2275
2276 #[cfg(feature = "plugins")]
2278 pub fn register_section_processor(
2279 &mut self,
2280 extension_name: String,
2281 processor: Box<dyn ass_core::plugin::SectionProcessor>,
2282 ) -> Result<()> {
2283 if self.registry_integration.is_none() {
2284 self.initialize_registry()?;
2285 }
2286
2287 let registry_ref =
2288 self.registry_integration
2289 .as_mut()
2290 .ok_or_else(|| EditorError::ExtensionError {
2291 extension: extension_name.clone(),
2292 message: "Registry integration not available".to_string(),
2293 })?;
2294
2295 if let Some(integration) = Arc::get_mut(registry_ref) {
2296 integration.register_custom_section_processor(extension_name, processor)
2297 } else {
2298 Err(EditorError::ExtensionError {
2299 extension: extension_name,
2300 message: "Cannot modify shared registry integration".to_string(),
2301 })
2302 }
2303 }
2304}
2305
2306#[cfg(test)]
2307mod tests {
2308 use super::*;
2309 #[cfg(not(feature = "std"))]
2310 use alloc::string::ToString;
2311 #[cfg(not(feature = "std"))]
2312 use alloc::vec;
2313
2314 #[test]
2315 fn document_creation() {
2316 let doc = EditorDocument::new();
2317 assert!(doc.is_empty());
2318 assert_eq!(doc.len_lines(), 1);
2319 assert!(!doc.is_modified());
2320 }
2321
2322 #[test]
2323 fn document_from_content() {
2324 let content = "[Script Info]\nTitle: Test";
2325 let doc = EditorDocument::from_content(content).unwrap();
2326 assert_eq!(doc.text(), content);
2327 assert_eq!(doc.len_bytes(), content.len());
2328 }
2329
2330 #[test]
2331 fn document_modification() {
2332 let mut doc = EditorDocument::new();
2333 doc.insert(Position::new(0), "Hello").unwrap();
2334 assert!(doc.is_modified());
2335 assert_eq!(doc.text(), "Hello");
2336 }
2337
2338 #[test]
2339 fn position_conversion() {
2340 let content = "Line 1\nLine 2\nLine 3";
2341 let doc = EditorDocument::from_content(content).unwrap();
2342
2343 let pos = Position::new(7); let lc = doc.position_to_line_column(pos).unwrap();
2346 assert_eq!(lc.line, 2);
2347 assert_eq!(lc.column, 1);
2348 }
2349
2350 #[test]
2351 fn range_operations() {
2352 let mut doc = EditorDocument::from_content("Hello World").unwrap();
2353
2354 let range = Range::new(Position::new(6), Position::new(11));
2356 doc.delete(range).unwrap();
2357 assert_eq!(doc.text(), "Hello ");
2358
2359 doc.insert(Position::new(6), "Rust").unwrap();
2361 assert_eq!(doc.text(), "Hello Rust");
2362 }
2363
2364 #[test]
2365 fn parse_script_test() {
2366 let content = "[Script Info]\nTitle: Test\n[Events]\nDialogue: test";
2367 let doc = EditorDocument::from_content(content).unwrap();
2368
2369 doc.validate().unwrap();
2371
2372 let sections_count = doc.sections_count().unwrap();
2374 assert!(sections_count > 0);
2375 }
2376
2377 #[test]
2378 fn test_edit_event_by_index() {
2379 let content = r#"[Script Info]
2380Title: Test
2381
2382[Events]
2383Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
2384Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,First event
2385Dialogue: 0,0:00:05.00,0:00:10.00,Default,,0,0,0,,Second event
2386Dialogue: 0,0:00:10.00,0:00:15.00,Default,,0,0,0,,Third event"#;
2387
2388 let mut doc = EditorDocument::from_content(content).unwrap();
2389
2390 let result = doc.edit_event_by_index(1, |_event| {
2392 vec![
2393 ("text", "Modified second event".to_string()),
2394 ("style", "NewStyle".to_string()),
2395 ("start", "0:00:06.00".to_string()),
2396 ]
2397 });
2398
2399 assert!(result.is_ok());
2400 let new_line = result.unwrap();
2401 assert!(new_line.contains("Modified second event"));
2402 assert!(new_line.contains("NewStyle"));
2403 assert!(new_line.contains("0:00:06.00"));
2404
2405 let updated_content = doc.text();
2407 assert!(updated_content.contains("Modified second event"));
2408 assert!(updated_content.contains("NewStyle"));
2409 assert!(updated_content.contains("0:00:06.00"));
2410 assert!(!updated_content.contains("Second event")); }
2412
2413 #[test]
2414 fn test_edit_event_by_index_out_of_bounds() {
2415 let content = r#"[Script Info]
2416Title: Test
2417
2418[Events]
2419Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
2420Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,First event"#;
2421
2422 let mut doc = EditorDocument::from_content(content).unwrap();
2423
2424 let result =
2426 doc.edit_event_by_index(5, |_event| vec![("text", "This should fail".to_string())]);
2427
2428 assert!(result.is_err());
2429 match result.err().unwrap() {
2430 EditorError::InvalidRange { start, end, length } => {
2431 assert_eq!(start, 5);
2432 assert_eq!(end, 6);
2433 assert_eq!(length, 1); }
2435 _ => panic!("Expected InvalidRange error"),
2436 }
2437 }
2438
2439 #[test]
2440 fn test_edit_event_with_builder() {
2441 let content = r#"[Script Info]
2442Title: Test
2443
2444[Events]
2445Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
2446Dialogue: 0,0:00:00.00,0:00:05.00,Default,Speaker,0,0,0,,Original text
2447Dialogue: 0,0:00:05.00,0:00:10.00,Default,,0,0,0,,Second event"#;
2448
2449 let mut doc = EditorDocument::from_content(content).unwrap();
2450
2451 let result = doc.edit_event_with_builder(0, |builder| {
2453 builder
2454 .text("Modified with builder")
2455 .style("NewStyle")
2456 .end_time("0:00:08.00")
2457 .speaker("NewSpeaker")
2458 });
2459
2460 assert!(result.is_ok());
2461 let new_line = result.unwrap();
2462 assert!(new_line.contains("Modified with builder"));
2463 assert!(new_line.contains("NewStyle"));
2464 assert!(new_line.contains("0:00:08.00"));
2465 assert!(new_line.contains("NewSpeaker"));
2466
2467 let updated_content = doc.text();
2469 assert!(updated_content.contains("Modified with builder"));
2470 assert!(updated_content.contains("0:00:08.00"));
2471 assert!(!updated_content.contains("Original text"));
2472 }
2473
2474 #[test]
2475 fn test_edit_event_with_builder_preserves_format() {
2476 let content = r#"[Script Info]
2478Title: Test
2479
2480[Events]
2481Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginT, MarginB, Effect, Text
2482Dialogue: 0,0:00:00.00,0:00:05.00,Default,,10,20,5,15,fade,Original text"#;
2483
2484 let mut doc = EditorDocument::from_content(content).unwrap();
2485
2486 let result = doc.edit_event_with_builder(0, |builder| {
2488 builder.text("New text").margin_top(30).margin_bottom(40)
2489 });
2490
2491 assert!(result.is_ok());
2492 let new_line = result.unwrap();
2493
2494 assert!(new_line.contains("30")); assert!(new_line.contains("40")); assert!(new_line.contains("New text"));
2498 assert!(new_line.contains("10,20,30,40")); }
2500
2501 #[test]
2502 fn test_edit_event_with_builder_comment() {
2503 let content = r#"[Script Info]
2504Title: Test
2505
2506[Events]
2507Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
2508Comment: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,This is a comment"#;
2509
2510 let mut doc = EditorDocument::from_content(content).unwrap();
2511
2512 let result = doc.edit_event_with_builder(0, |builder| builder.text("Updated comment"));
2514
2515 assert!(result.is_ok());
2516 let new_line = result.unwrap();
2517 assert!(new_line.starts_with("Comment:"));
2518 assert!(new_line.contains("Updated comment"));
2519 }
2520
2521 #[test]
2522 fn test_edit_event_by_index_all_fields() {
2523 let content = r#"[Script Info]
2524Title: Test
2525
2526[Events]
2527Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
2528Dialogue: 0,0:00:00.00,0:00:05.00,Default,Speaker,10,20,30,fade,Original text"#;
2529
2530 let mut doc = EditorDocument::from_content(content).unwrap();
2531
2532 let result = doc.edit_event_by_index(0, |_event| {
2534 vec![
2535 ("layer", "1".to_string()),
2536 ("start", "0:00:01.00".to_string()),
2537 ("end", "0:00:06.00".to_string()),
2538 ("style", "Custom".to_string()),
2539 ("name", "NewSpeaker".to_string()),
2540 ("margin_l", "15".to_string()),
2541 ("margin_r", "25".to_string()),
2542 ("margin_v", "35".to_string()),
2543 ("effect", "scroll".to_string()),
2544 ("text", "Completely new text".to_string()),
2545 ]
2546 });
2547
2548 assert!(result.is_ok());
2549 let new_line = result.unwrap();
2550
2551 assert!(new_line.contains("Dialogue: 1,"));
2553 assert!(new_line.contains("0:00:01.00"));
2554 assert!(new_line.contains("0:00:06.00"));
2555 assert!(new_line.contains("Custom"));
2556 assert!(new_line.contains("NewSpeaker"));
2557 assert!(new_line.contains("15"));
2558 assert!(new_line.contains("25"));
2559 assert!(new_line.contains("35"));
2560 assert!(new_line.contains("scroll"));
2561 assert!(new_line.contains("Completely new text"));
2562 }
2563
2564 #[test]
2565 fn test_undo_redo_basic() {
2566 let mut doc = EditorDocument::from_content("[Script Info]\nTitle: Test").unwrap();
2567 let initial_len = doc.len_bytes();
2568 doc.insert(Position::new(initial_len), "\nAuthor: John")
2576 .unwrap();
2577 assert!(doc.text().contains("Author: John"));
2583 assert!(doc.can_undo());
2584 assert!(!doc.can_redo());
2585
2586 let result = doc.undo().unwrap();
2588 assert!(result.success);
2594 assert!(!doc.text().contains("Author: John"));
2595 assert!(!doc.can_undo());
2596 assert!(doc.can_redo());
2597
2598 let result = doc.redo().unwrap();
2601 assert!(result.success);
2607 assert!(doc.text().contains("Author: John"));
2608 assert!(doc.can_undo());
2609 assert!(!doc.can_redo());
2610 }
2611
2612 #[test]
2613 fn test_undo_redo_multiple_operations() {
2614 let mut doc = EditorDocument::from_content("[Script Info]\nTitle: Test").unwrap();
2615
2616 doc.insert(Position::new(doc.len_bytes()), "\nAuthor: John")
2618 .unwrap();
2619 doc.insert(Position::new(doc.len_bytes()), "\nVersion: 1.0")
2620 .unwrap();
2621 doc.insert(Position::new(doc.len_bytes()), "\nComment: Test script")
2622 .unwrap();
2623
2624 assert!(doc.text().contains("Author: John"));
2625 assert!(doc.text().contains("Version: 1.0"));
2626 assert!(doc.text().contains("Comment: Test script"));
2627
2628 doc.undo().unwrap();
2630 assert!(!doc.text().contains("Comment: Test script"));
2631
2632 doc.undo().unwrap();
2633 assert!(!doc.text().contains("Version: 1.0"));
2634
2635 doc.undo().unwrap();
2636 assert!(!doc.text().contains("Author: John"));
2637
2638 doc.redo().unwrap();
2640 assert!(doc.text().contains("Author: John"));
2641 assert!(!doc.text().contains("Version: 1.0"));
2642 }
2643
2644 #[test]
2645 fn test_undo_redo_replace() {
2646 let mut doc = EditorDocument::from_content("[Script Info]\nTitle: Original").unwrap();
2647
2648 let start = doc.text().find("Original").unwrap();
2650 let range = Range::new(Position::new(start), Position::new(start + 8));
2651 doc.replace(range, "Modified").unwrap();
2652
2653 assert!(doc.text().contains("Title: Modified"));
2654 assert!(!doc.text().contains("Original"));
2655
2656 doc.undo().unwrap();
2658 assert!(doc.text().contains("Title: Original"));
2659 assert!(!doc.text().contains("Modified"));
2660
2661 doc.redo().unwrap();
2663 assert!(doc.text().contains("Title: Modified"));
2664 assert!(!doc.text().contains("Original"));
2665 }
2666
2667 #[test]
2668 fn test_validator_integration() {
2669 let mut doc = EditorDocument::from_content(
2670 "[Script Info]\nTitle: Test\n\n[V4+ Styles]\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,Test"
2671 ).unwrap();
2672
2673 let result = doc.validate_comprehensive().unwrap();
2675 assert!(result.is_valid);
2676
2677 doc.insert(Position::new(doc.len_bytes()), "\nComment: Test")
2679 .unwrap();
2680
2681 let result2 = doc.force_validate().unwrap();
2683 assert!(result2.is_valid);
2684 }
2685
2686 #[test]
2687 fn test_validator_configuration() {
2688 let mut doc = EditorDocument::new();
2689
2690 let config = crate::utils::validator::ValidatorConfig {
2692 max_issues: 5,
2693 enable_performance_hints: false,
2694 ..Default::default()
2695 };
2696 doc.set_validator_config(config);
2697
2698 assert!(doc.is_valid_cached().is_ok());
2701 }
2702
2703 #[test]
2704 fn test_validator_with_invalid_document() {
2705 let mut doc = EditorDocument::from_content("Invalid content").unwrap();
2706
2707 let result = doc.validate_comprehensive().unwrap();
2709 assert!(!result.issues.is_empty());
2710
2711 let warnings =
2713 result.issues_with_severity(crate::utils::validator::ValidationSeverity::Warning);
2714 assert!(!warnings.is_empty());
2715 }
2716
2717 #[test]
2718 #[cfg(feature = "formats")]
2719 fn test_format_import_export() {
2720 let srt_content = "1\n00:00:00,000 --> 00:00:05,000\nHello world!";
2722 let doc = EditorDocument::import_format(
2723 srt_content,
2724 Some(crate::utils::formats::SubtitleFormat::SRT),
2725 )
2726 .unwrap();
2727 assert!(doc.text().contains("Hello world!"));
2728 assert!(doc.has_events().unwrap());
2729
2730 let options = crate::utils::formats::ConversionOptions::default();
2732 let webvtt = doc
2733 .export_format(crate::utils::formats::SubtitleFormat::WebVTT, &options)
2734 .unwrap();
2735 assert!(webvtt.starts_with("WEBVTT"));
2736 assert!(webvtt.contains("00:00:00.000 --> 00:00:05.000"));
2737 assert!(webvtt.contains("Hello world!"));
2738 }
2739
2740 #[test]
2741 fn test_undo_redo_delete() {
2742 let mut doc =
2743 EditorDocument::from_content("[Script Info]\nTitle: Test\nAuthor: John").unwrap();
2744
2745 let start = doc.text().find("\nAuthor: John").unwrap();
2747 let range = Range::new(Position::new(start), Position::new(start + 13));
2748 doc.delete(range).unwrap();
2749
2750 assert!(!doc.text().contains("Author: John"));
2751
2752 doc.undo().unwrap();
2754 assert!(doc.text().contains("Author: John"));
2755
2756 doc.redo().unwrap();
2758 assert!(!doc.text().contains("Author: John"));
2759 }
2760
2761 #[cfg(feature = "plugins")]
2762 #[test]
2763 fn test_registry_integration() {
2764 let mut doc = EditorDocument::new();
2765
2766 assert!(doc.registry().is_none());
2768
2769 doc.initialize_registry().unwrap();
2771 assert!(doc.registry().is_some());
2772
2773 doc.insert(Position::new(0), "[Script Info]\nTitle: Test\n\n[Events]\nDialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,{\\b1}Bold{\\b0} text").unwrap();
2775
2776 let section_count = doc
2777 .parse_with_extensions(|script| script.sections().len())
2778 .unwrap();
2779 assert_eq!(section_count, 2);
2780 }
2781
2782 #[cfg(feature = "plugins")]
2783 #[test]
2784 fn test_custom_tag_handler() {
2785 use ass_core::plugin::{TagHandler, TagResult};
2786
2787 struct CustomHandler;
2788 impl TagHandler for CustomHandler {
2789 fn name(&self) -> &'static str {
2790 "custom"
2791 }
2792
2793 fn process(&self, _args: &str) -> TagResult {
2794 TagResult::Processed
2795 }
2796
2797 fn validate(&self, _args: &str) -> bool {
2798 true
2799 }
2800 }
2801
2802 let mut doc = EditorDocument::new();
2803 doc.initialize_registry().unwrap();
2804
2805 assert!(doc
2807 .register_tag_handler("test-extension".to_string(), Box::new(CustomHandler))
2808 .is_ok());
2809 }
2810
2811 #[cfg(feature = "plugins")]
2812 #[test]
2813 fn test_custom_section_processor() {
2814 use ass_core::plugin::{SectionProcessor, SectionResult};
2815
2816 struct CustomProcessor;
2817 impl SectionProcessor for CustomProcessor {
2818 fn name(&self) -> &'static str {
2819 "CustomSection"
2820 }
2821
2822 fn process(&self, _header: &str, _lines: &[&str]) -> SectionResult {
2823 SectionResult::Processed
2824 }
2825
2826 fn validate(&self, _header: &str, _lines: &[&str]) -> bool {
2827 true
2828 }
2829 }
2830
2831 let mut doc = EditorDocument::new();
2832 doc.initialize_registry().unwrap();
2833
2834 assert!(doc
2836 .register_section_processor("test-extension".to_string(), Box::new(CustomProcessor))
2837 .is_ok());
2838 }
2839}