1use super::{CommandResult, EditorCommand};
7use crate::core::{EditorDocument, EditorError, Position, Range, Result, StyleBuilder};
8
9#[cfg(not(feature = "std"))]
10use alloc::{
11 format,
12 string::{String, ToString},
13 vec::Vec,
14};
15
16#[cfg(feature = "std")]
17use std::collections::HashMap;
18
19#[cfg(not(feature = "std"))]
20use alloc::collections::BTreeMap as HashMap;
21
22#[derive(Debug, Clone)]
24pub struct CreateStyleCommand {
25 pub style_name: String,
26 pub style_builder: StyleBuilder,
27 pub description: Option<String>,
28}
29
30impl CreateStyleCommand {
31 pub fn new(style_name: String, style_builder: StyleBuilder) -> Self {
33 Self {
34 style_name,
35 style_builder,
36 description: None,
37 }
38 }
39
40 #[must_use]
42 pub fn with_description(mut self, description: String) -> Self {
43 self.description = Some(description);
44 self
45 }
46}
47
48impl EditorCommand for CreateStyleCommand {
49 fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
50 let mut builder = self.style_builder.clone();
52 builder = builder.name(&self.style_name);
53 let style_line = builder.build()?;
54
55 let content = document.text();
57 let styles_section_pos = content
58 .find("[V4+ Styles]")
59 .or_else(|| content.find("[V4 Styles]"))
60 .or_else(|| content.find("[Styles]"));
61
62 if let Some(section_start) = styles_section_pos {
63 let section_content = &content[section_start..];
65 if let Some(format_line_end) = section_content.find('\n') {
66 let format_end_pos = section_start + format_line_end + 1;
67
68 let insert_pos = if let Some(next_line_start) = content[format_end_pos..].find('\n')
70 {
71 format_end_pos + next_line_start + 1
72 } else {
73 format_end_pos
74 };
75
76 let insert_text = format!("{style_line}\n");
78 document.insert(Position::new(insert_pos), &insert_text)?;
79
80 let end_pos = Position::new(insert_pos + insert_text.len());
81 return Ok(CommandResult::success_with_change(
82 Range::new(Position::new(insert_pos), end_pos),
83 end_pos,
84 )
85 .with_message(format!("Created style '{}'", self.style_name)));
86 }
87 }
88
89 let styles_section = format!(
91 "\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"
92 );
93
94 let insert_pos = if let Some(events_pos) = content.find("[Events]") {
96 events_pos
97 } else {
98 content.len()
99 };
100
101 document.insert(Position::new(insert_pos), &styles_section)?;
102
103 let end_pos = Position::new(insert_pos + styles_section.len());
104 Ok(CommandResult::success_with_change(
105 Range::new(Position::new(insert_pos), end_pos),
106 end_pos,
107 )
108 .with_message(format!(
109 "Created styles section and style '{}'",
110 self.style_name
111 )))
112 }
113
114 fn description(&self) -> &str {
115 self.description.as_deref().unwrap_or("Create style")
116 }
117
118 fn memory_usage(&self) -> usize {
119 core::mem::size_of::<Self>()
120 + self.style_name.len()
121 + self.description.as_ref().map_or(0, |d| d.len())
122 + 200 }
124}
125
126#[derive(Debug, Clone)]
128pub struct EditStyleCommand {
129 pub style_name: String,
130 pub field_updates: HashMap<String, String>,
131 pub description: Option<String>,
132}
133
134impl EditStyleCommand {
135 pub fn new(style_name: String) -> Self {
137 Self {
138 style_name,
139 field_updates: HashMap::new(),
140 description: None,
141 }
142 }
143
144 pub fn set_field(mut self, field: &str, value: String) -> Self {
146 self.field_updates.insert(field.to_string(), value);
147 self
148 }
149
150 pub fn set_font(self, font: &str) -> Self {
152 self.set_field("Fontname", font.to_string())
153 }
154
155 pub fn set_size(self, size: u32) -> Self {
157 self.set_field("Fontsize", size.to_string())
158 }
159
160 pub fn set_color(self, color: &str) -> Self {
162 self.set_field("PrimaryColour", color.to_string())
163 }
164
165 pub fn set_bold(self, bold: bool) -> Self {
167 self.set_field("Bold", if bold { "-1" } else { "0" }.to_string())
168 }
169
170 pub fn set_italic(self, italic: bool) -> Self {
172 self.set_field("Italic", if italic { "-1" } else { "0" }.to_string())
173 }
174
175 pub fn set_alignment(self, alignment: u32) -> Self {
177 self.set_field("Alignment", alignment.to_string())
178 }
179
180 #[must_use]
182 pub fn with_description(mut self, description: String) -> Self {
183 self.description = Some(description);
184 self
185 }
186}
187
188impl EditorCommand for EditStyleCommand {
189 fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
190 let content = document.text();
191 let style_pattern = format!("Style: {}", self.style_name);
192
193 if let Some(style_start) = content.find(&style_pattern) {
194 let line_start = content[..style_start]
196 .rfind('\n')
197 .map(|pos| pos + 1)
198 .unwrap_or(0);
199 let line_end = content[style_start..]
200 .find('\n')
201 .map(|pos| style_start + pos)
202 .unwrap_or(content.len());
203
204 let style_line = &content[line_start..line_end];
205 let fields: Vec<&str> = style_line.split(',').collect();
206
207 if fields.len() < 2 {
208 return Err(EditorError::command_failed("Invalid style format"));
209 }
210
211 let styles_section_start = content[..line_start]
213 .rfind("[V4+ Styles]")
214 .or_else(|| content[..line_start].rfind("[V4 Styles]"))
215 .or_else(|| content[..line_start].rfind("[Styles]"))
216 .ok_or_else(|| EditorError::command_failed("Could not find styles section"))?;
217
218 let format_line_start = content[styles_section_start..]
219 .find("Format:")
220 .map(|pos| styles_section_start + pos)
221 .ok_or_else(|| EditorError::command_failed("Could not find format line"))?;
222
223 let format_line_end = content[format_line_start..]
224 .find('\n')
225 .map(|pos| format_line_start + pos)
226 .unwrap_or(content.len());
227
228 let format_line = &content[format_line_start..format_line_end];
229 let format_fields: Vec<&str> = format_line
230 .strip_prefix("Format: ")
231 .unwrap_or(format_line)
232 .split(", ")
233 .collect();
234
235 let mut updated_fields = fields
237 .iter()
238 .map(|f| f.to_string())
239 .collect::<Vec<String>>();
240
241 for (field_name, new_value) in &self.field_updates {
242 if let Some(field_index) = format_fields.iter().position(|f| f == field_name) {
243 if field_index < updated_fields.len() {
244 updated_fields[field_index] = new_value.clone();
245 }
246 }
247 }
248
249 let new_style_line = updated_fields.join(",");
250 let range = Range::new(Position::new(line_start), Position::new(line_end));
251
252 document.replace(range, &new_style_line)?;
253
254 let end_pos = Position::new(line_start + new_style_line.len());
255 Ok(CommandResult::success_with_change(
256 Range::new(Position::new(line_start), end_pos),
257 end_pos,
258 )
259 .with_message(format!("Updated style '{}'", self.style_name)))
260 } else {
261 Err(EditorError::command_failed(format!(
262 "Style '{}' not found",
263 self.style_name
264 )))
265 }
266 }
267
268 fn description(&self) -> &str {
269 self.description.as_deref().unwrap_or("Edit style")
270 }
271
272 fn memory_usage(&self) -> usize {
273 core::mem::size_of::<Self>()
274 + self.style_name.len()
275 + self
276 .field_updates
277 .iter()
278 .map(|(k, v)| k.len() + v.len())
279 .sum::<usize>()
280 + self.description.as_ref().map_or(0, |d| d.len())
281 }
282}
283
284#[derive(Debug, Clone)]
286pub struct DeleteStyleCommand {
287 pub style_name: String,
288 pub description: Option<String>,
289}
290
291impl DeleteStyleCommand {
292 pub fn new(style_name: String) -> Self {
294 Self {
295 style_name,
296 description: None,
297 }
298 }
299
300 #[must_use]
302 pub fn with_description(mut self, description: String) -> Self {
303 self.description = Some(description);
304 self
305 }
306}
307
308impl EditorCommand for DeleteStyleCommand {
309 fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
310 let content = document.text();
311 let style_pattern = format!("Style: {}", self.style_name);
312
313 if let Some(style_start) = content.find(&style_pattern) {
314 let line_start = content[..style_start]
316 .rfind('\n')
317 .map(|pos| pos + 1)
318 .unwrap_or(0);
319 let line_end = content[style_start..]
320 .find('\n')
321 .map(|pos| style_start + pos + 1) .unwrap_or(content.len());
323
324 let range = Range::new(Position::new(line_start), Position::new(line_end));
325 document.delete(range)?;
326
327 Ok(CommandResult::success_with_change(
328 Range::new(Position::new(line_start), Position::new(line_start)),
329 Position::new(line_start),
330 )
331 .with_message(format!("Deleted style '{}'", self.style_name)))
332 } else {
333 Err(EditorError::command_failed(format!(
334 "Style '{}' not found",
335 self.style_name
336 )))
337 }
338 }
339
340 fn description(&self) -> &str {
341 self.description.as_deref().unwrap_or("Delete style")
342 }
343
344 fn memory_usage(&self) -> usize {
345 core::mem::size_of::<Self>()
346 + self.style_name.len()
347 + self.description.as_ref().map_or(0, |d| d.len())
348 }
349}
350
351#[derive(Debug, Clone)]
353pub struct CloneStyleCommand {
354 pub source_style: String,
355 pub target_style: String,
356 pub description: Option<String>,
357}
358
359impl CloneStyleCommand {
360 pub fn new(source_style: String, target_style: String) -> Self {
362 Self {
363 source_style,
364 target_style,
365 description: None,
366 }
367 }
368
369 #[must_use]
371 pub fn with_description(mut self, description: String) -> Self {
372 self.description = Some(description);
373 self
374 }
375}
376
377impl EditorCommand for CloneStyleCommand {
378 fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
379 let content = document.text();
380 let source_pattern = format!("Style: {}", self.source_style);
381
382 let target_pattern = format!("Style: {}", self.target_style);
384 if content.contains(&target_pattern) {
385 return Err(EditorError::command_failed(format!(
386 "Style '{}' already exists",
387 self.target_style
388 )));
389 }
390
391 if let Some(source_start) = content.find(&source_pattern) {
392 let line_start = content[..source_start]
394 .rfind('\n')
395 .map(|pos| pos + 1)
396 .unwrap_or(0);
397 let line_end = content[source_start..]
398 .find('\n')
399 .map(|pos| source_start + pos)
400 .unwrap_or(content.len());
401
402 let source_line = &content[line_start..line_end];
403
404 let cloned_line = source_line.replace(
406 &format!("Style: {}", self.source_style),
407 &format!("Style: {}", self.target_style),
408 );
409
410 let insert_pos = line_end;
412 let insert_text = format!("\n{cloned_line}");
413
414 document.insert(Position::new(insert_pos), &insert_text)?;
415
416 let end_pos = Position::new(insert_pos + insert_text.len());
417 Ok(CommandResult::success_with_change(
418 Range::new(Position::new(insert_pos), end_pos),
419 end_pos,
420 )
421 .with_message(format!(
422 "Cloned style '{}' to '{}'",
423 self.source_style, self.target_style
424 )))
425 } else {
426 Err(EditorError::command_failed(format!(
427 "Source style '{}' not found",
428 self.source_style
429 )))
430 }
431 }
432
433 fn description(&self) -> &str {
434 self.description.as_deref().unwrap_or("Clone style")
435 }
436
437 fn memory_usage(&self) -> usize {
438 core::mem::size_of::<Self>()
439 + self.source_style.len()
440 + self.target_style.len()
441 + self.description.as_ref().map_or(0, |d| d.len())
442 }
443}
444
445#[derive(Debug, Clone)]
447pub struct ApplyStyleCommand {
448 pub old_style: String,
449 pub new_style: String,
450 pub event_filter: Option<String>, pub description: Option<String>,
452}
453
454impl ApplyStyleCommand {
455 pub fn new(old_style: String, new_style: String) -> Self {
457 Self {
458 old_style,
459 new_style,
460 event_filter: None,
461 description: None,
462 }
463 }
464
465 pub fn with_filter(mut self, filter: String) -> Self {
467 self.event_filter = Some(filter);
468 self
469 }
470
471 #[must_use]
473 pub fn with_description(mut self, description: String) -> Self {
474 self.description = Some(description);
475 self
476 }
477}
478
479impl EditorCommand for ApplyStyleCommand {
480 fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
481 let mut content = document.text();
482 let mut changes_made = 0;
483 let mut total_range: Option<Range> = None;
484
485 let events_start = content
487 .find("[Events]")
488 .ok_or_else(|| EditorError::command_failed("Events section not found"))?;
489
490 let events_content_start = content[events_start..]
492 .find('\n')
493 .map(|pos| events_start + pos + 1)
494 .ok_or_else(|| EditorError::command_failed("Invalid events section format"))?;
495
496 let first_event_start = content[events_content_start..]
498 .find('\n')
499 .map(|pos| events_content_start + pos + 1)
500 .unwrap_or(events_content_start);
501
502 let mut search_pos = first_event_start;
503
504 while search_pos < content.len() {
505 let line_start = search_pos;
507 let line_end = content[line_start..]
508 .find('\n')
509 .map(|pos| line_start + pos)
510 .unwrap_or(content.len());
511
512 if line_start >= line_end {
513 break;
514 }
515
516 let line = &content[line_start..line_end];
517
518 if line.starts_with("Dialogue:") || line.starts_with("Comment:") {
520 let parts: Vec<&str> = line.split(',').collect();
521
522 if parts.len() > 4 && parts[3].trim() == self.old_style {
524 let should_update = if let Some(ref filter) = self.event_filter {
526 line.contains(filter)
527 } else {
528 true
529 };
530
531 if should_update {
532 let updated_line = line.replace(
534 &format!(",{},", self.old_style),
535 &format!(",{},", self.new_style),
536 );
537
538 let range = Range::new(Position::new(line_start), Position::new(line_end));
540 document.replace(range, &updated_line)?;
541
542 content = document.text();
544
545 let change_range = Range::new(
547 Position::new(line_start),
548 Position::new(line_start + updated_line.len()),
549 );
550 total_range = Some(match total_range {
551 Some(existing) => existing.union(&change_range),
552 None => change_range,
553 });
554
555 changes_made += 1;
556 }
557 }
558 } else if line.starts_with('[') && line != "[Events]" {
559 break;
561 }
562
563 search_pos = line_end + 1;
564 }
565
566 if changes_made > 0 {
567 Ok(CommandResult::success_with_change(
568 total_range.unwrap_or(Range::new(Position::new(0), Position::new(0))),
569 Position::new(content.len()),
570 )
571 .with_message(format!(
572 "Applied style '{}' to {} events",
573 self.new_style, changes_made
574 )))
575 } else {
576 Ok(CommandResult::success()
577 .with_message("No events found matching the criteria".to_string()))
578 }
579 }
580
581 fn description(&self) -> &str {
582 self.description
583 .as_deref()
584 .unwrap_or("Apply style to events")
585 }
586
587 fn memory_usage(&self) -> usize {
588 core::mem::size_of::<Self>()
589 + self.old_style.len()
590 + self.new_style.len()
591 + self.event_filter.as_ref().map_or(0, |f| f.len())
592 + self.description.as_ref().map_or(0, |d| d.len())
593 }
594}
595
596#[cfg(test)]
597mod tests {
598 use super::*;
599 use crate::core::EditorDocument;
600 #[cfg(not(feature = "std"))]
601 use alloc::string::ToString;
602 const TEST_CONTENT: &str = r#"[Script Info]
603Title: Test Script
604
605[V4+ Styles]
606Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
607Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
608
609[Events]
610Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
611Dialogue: 0,0:00:01.00,0:00:05.00,Default,Speaker,0,0,0,,Hello world!
612"#;
613
614 #[test]
615 fn test_create_style_command() {
616 let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
617
618 let style_builder = StyleBuilder::new()
619 .font("Comic Sans MS")
620 .size(24)
621 .bold(true);
622
623 let command = CreateStyleCommand::new("NewStyle".to_string(), style_builder);
624 let result = command.execute(&mut doc).unwrap();
625
626 assert!(result.success);
627 assert!(result.content_changed);
628 assert!(doc.text().contains("Style: NewStyle"));
629 assert!(doc.text().contains("Comic Sans MS"));
630 }
631
632 #[test]
633 fn test_edit_style_command() {
634 let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
635
636 let command = EditStyleCommand::new("Default".to_string())
637 .set_font("Helvetica")
638 .set_size(24)
639 .set_bold(true);
640
641 let result = command.execute(&mut doc).unwrap();
642
643 assert!(result.success);
644 assert!(result.content_changed);
645 assert!(doc.text().contains("Helvetica"));
646 assert!(doc.text().contains("24"));
647 assert!(doc.text().contains("-1")); }
649
650 #[test]
651 fn test_delete_style_command() {
652 let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
653
654 let command = DeleteStyleCommand::new("Default".to_string());
655 let result = command.execute(&mut doc).unwrap();
656
657 assert!(result.success);
658 assert!(result.content_changed);
659 assert!(!doc.text().contains("Style: Default"));
660 }
661
662 #[test]
663 fn test_clone_style_command() {
664 let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
665
666 let command = CloneStyleCommand::new("Default".to_string(), "DefaultCopy".to_string());
667 let result = command.execute(&mut doc).unwrap();
668
669 assert!(result.success);
670 assert!(result.content_changed);
671 assert!(doc.text().contains("Style: Default")); assert!(doc.text().contains("Style: DefaultCopy")); }
674
675 #[test]
676 fn test_apply_style_command() {
677 let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
678
679 let create_cmd = CreateStyleCommand::new(
681 "NewStyle".to_string(),
682 StyleBuilder::new().font("Verdana").size(18),
683 );
684 create_cmd.execute(&mut doc).unwrap();
685
686 let command = ApplyStyleCommand::new("Default".to_string(), "NewStyle".to_string());
688 let result = command.execute(&mut doc).unwrap();
689
690 assert!(result.success);
691 assert!(result.content_changed);
692 assert!(doc.text().contains("NewStyle")); }
694
695 #[test]
696 fn test_apply_style_with_filter() {
697 let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
698
699 let create_cmd = CreateStyleCommand::new(
701 "FilteredStyle".to_string(),
702 StyleBuilder::new().font("Times").size(22),
703 );
704 create_cmd.execute(&mut doc).unwrap();
705
706 let command = ApplyStyleCommand::new("Default".to_string(), "FilteredStyle".to_string())
708 .with_filter("Hello".to_string());
709
710 let result = command.execute(&mut doc).unwrap();
711
712 assert!(result.success);
713 assert!(result.content_changed);
714 assert!(doc.text().contains("FilteredStyle"));
715 }
716
717 #[test]
718 fn test_edit_nonexistent_style() {
719 let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
720
721 let command = EditStyleCommand::new("NonExistent".to_string()).set_font("Arial");
722
723 let result = command.execute(&mut doc);
724 assert!(result.is_err());
725 }
726
727 #[test]
728 fn test_clone_to_existing_style() {
729 let mut doc = EditorDocument::from_content(TEST_CONTENT).unwrap();
730
731 let command = CloneStyleCommand::new("Default".to_string(), "Default".to_string());
732 let result = command.execute(&mut doc);
733
734 assert!(result.is_err());
735 }
736}