ass_editor/core/
builders.rs

1//! Builder patterns for ASS types
2//!
3//! Provides fluent builder APIs for creating ASS events, styles, and other structures
4//! with ergonomic method chaining and validation.
5
6use crate::core::errors::{EditorError, Result};
7use ass_core::parser::ast::EventType;
8use ass_core::ScriptVersion;
9
10#[cfg(feature = "std")]
11use std::borrow::Cow;
12
13#[cfg(not(feature = "std"))]
14use alloc::{borrow::Cow, format, string::ToString, vec};
15
16#[cfg(not(feature = "std"))]
17use alloc::{string::String, vec::Vec};
18
19/// Builder for creating ASS events with fluent API
20///
21/// Provides an ergonomic way to construct ASS events with method chaining.
22/// Supports all event types and automatically handles format validation.
23///
24/// # Examples
25///
26/// ```
27/// use ass_editor::{EventBuilder, EditorDocument};
28///
29/// let mut doc = EditorDocument::new();
30///
31/// // Create a dialogue event
32/// let event_line = EventBuilder::dialogue()
33///     .start_time("0:00:00.00")
34///     .end_time("0:00:05.00")
35///     .style("Default")
36///     .speaker("Character")
37///     .text("Hello, world!")
38///     .layer(0)
39///     .build()
40///     .unwrap();
41///
42/// // Add to document
43/// doc.add_event_line(&event_line).unwrap();
44/// ```
45#[derive(Debug, Default)]
46pub struct EventBuilder<'a> {
47    event_type: Option<EventType>,
48    start: Option<Cow<'a, str>>,
49    end: Option<Cow<'a, str>>,
50    style: Option<Cow<'a, str>>,
51    name: Option<Cow<'a, str>>,
52    text: Option<Cow<'a, str>>,
53    layer: Option<Cow<'a, str>>,
54    margin_l: Option<Cow<'a, str>>,
55    margin_r: Option<Cow<'a, str>>,
56    margin_v: Option<Cow<'a, str>>,
57    margin_t: Option<Cow<'a, str>>,
58    margin_b: Option<Cow<'a, str>>,
59    effect: Option<Cow<'a, str>>,
60}
61
62impl<'a> EventBuilder<'a> {
63    /// Create a new event builder
64    pub fn new() -> Self {
65        Self::default()
66    }
67
68    /// Create a dialogue event builder  
69    pub fn dialogue() -> Self {
70        Self {
71            event_type: Some(EventType::Dialogue),
72            ..Self::default()
73        }
74    }
75
76    /// Create a comment event builder
77    pub fn comment() -> Self {
78        Self {
79            event_type: Some(EventType::Comment),
80            ..Self::default()
81        }
82    }
83
84    /// Set start time (e.g., "0:00:05.00")
85    pub fn start_time<S: Into<Cow<'a, str>>>(mut self, time: S) -> Self {
86        self.start = Some(time.into());
87        self
88    }
89
90    /// Set end time (e.g., "0:00:10.00")
91    pub fn end_time<S: Into<Cow<'a, str>>>(mut self, time: S) -> Self {
92        self.end = Some(time.into());
93        self
94    }
95
96    /// Set speaker/character name
97    pub fn speaker<S: Into<Cow<'a, str>>>(mut self, name: S) -> Self {
98        self.name = Some(name.into());
99        self
100    }
101
102    /// Set dialogue text
103    pub fn text<S: Into<Cow<'a, str>>>(mut self, text: S) -> Self {
104        self.text = Some(text.into());
105        self
106    }
107
108    /// Set style name
109    pub fn style<S: Into<Cow<'a, str>>>(mut self, style: S) -> Self {
110        self.style = Some(style.into());
111        self
112    }
113
114    /// Set layer (higher layers render on top)
115    pub fn layer(mut self, layer: u32) -> Self {
116        self.layer = Some(Cow::Owned(layer.to_string()));
117        self
118    }
119
120    /// Set left margin
121    pub fn margin_left(mut self, margin: u32) -> Self {
122        self.margin_l = Some(Cow::Owned(margin.to_string()));
123        self
124    }
125
126    /// Set right margin
127    pub fn margin_right(mut self, margin: u32) -> Self {
128        self.margin_r = Some(Cow::Owned(margin.to_string()));
129        self
130    }
131
132    /// Set vertical margin  
133    pub fn margin_vertical(mut self, margin: u32) -> Self {
134        self.margin_v = Some(Cow::Owned(margin.to_string()));
135        self
136    }
137
138    /// Set top margin (V4++)
139    pub fn margin_top(mut self, margin: u32) -> Self {
140        self.margin_t = Some(Cow::Owned(margin.to_string()));
141        self
142    }
143
144    /// Set bottom margin (V4++)
145    pub fn margin_bottom(mut self, margin: u32) -> Self {
146        self.margin_b = Some(Cow::Owned(margin.to_string()));
147        self
148    }
149
150    /// Set effect
151    pub fn effect<S: Into<Cow<'a, str>>>(mut self, effect: S) -> Self {
152        self.effect = Some(effect.into());
153        self
154    }
155
156    /// Build the event (validates required fields)
157    pub fn build(self) -> Result<String> {
158        // Default to v4+ format
159        self.build_with_version(ScriptVersion::AssV4)
160    }
161
162    /// Build the event with a specific format version
163    pub fn build_with_version(self, version: ScriptVersion) -> Result<String> {
164        let event_type = self.event_type.unwrap_or(EventType::Dialogue);
165        let start = self.start.unwrap_or(Cow::Borrowed("0:00:00.00"));
166        let end = self.end.unwrap_or(Cow::Borrowed("0:00:05.00"));
167        let style = self.style.unwrap_or(Cow::Borrowed("Default"));
168        let name = self.name.unwrap_or(Cow::Borrowed(""));
169        let text = self.text.unwrap_or(Cow::Borrowed(""));
170        let layer = self.layer.unwrap_or(Cow::Borrowed("0"));
171        let margin_l = self.margin_l.unwrap_or(Cow::Borrowed("0"));
172        let margin_r = self.margin_r.unwrap_or(Cow::Borrowed("0"));
173        let margin_v = self.margin_v.unwrap_or(Cow::Borrowed("0"));
174        let effect = self.effect.unwrap_or(Cow::Borrowed(""));
175
176        // Format as ASS event line based on version
177        let event_type_str = event_type.as_str();
178        let line = match version {
179            ScriptVersion::SsaV4 => {
180                // SSA v4 format: no layer field, uses Marked=0 prefix
181                format!(
182                    "{event_type_str}: Marked=0,{start},{end},{style},{name},{margin_l},{margin_r},{margin_v},{effect},{text}"
183                )
184            }
185            ScriptVersion::AssV4 => {
186                // ASS v4 format: includes layer field
187                format!(
188                    "{event_type_str}: {layer},{start},{end},{style},{name},{margin_l},{margin_r},{margin_v},{effect},{text}"
189                )
190            }
191            ScriptVersion::AssV4Plus => {
192                // ASS v4++ format: can use margin_t/margin_b if specified
193                // For now, we use the same format as v4 since the builder doesn't support margin_t/margin_b yet
194                format!(
195                    "{event_type_str}: {layer},{start},{end},{style},{name},{margin_l},{margin_r},{margin_v},{effect},{text}"
196                )
197            }
198        };
199
200        Ok(line)
201    }
202
203    /// Build the event with a specific format line
204    /// The format parameter should contain field names like ["Layer", "Start", "End", "Style", "Text"]
205    pub fn build_with_format(&self, format: &[&str]) -> Result<String> {
206        if format.is_empty() {
207            return Err(EditorError::FormatLineError {
208                message: "Format line cannot be empty".to_string(),
209            });
210        }
211
212        let event_type = self.event_type.unwrap_or(EventType::Dialogue);
213        let event_type_str = event_type.as_str();
214
215        // Build field values based on format specification
216        let mut field_values = Vec::with_capacity(format.len());
217
218        for field in format {
219            let value = match *field {
220                "Layer" => self.layer.as_ref().map(|c| c.as_ref()).unwrap_or("0"),
221                "Start" => self
222                    .start
223                    .as_ref()
224                    .map(|c| c.as_ref())
225                    .unwrap_or("0:00:00.00"),
226                "End" => self
227                    .end
228                    .as_ref()
229                    .map(|c| c.as_ref())
230                    .unwrap_or("0:00:05.00"),
231                "Style" => self.style.as_ref().map(|c| c.as_ref()).unwrap_or("Default"),
232                "Name" | "Actor" => self.name.as_ref().map(|c| c.as_ref()).unwrap_or(""),
233                "MarginL" => self.margin_l.as_ref().map(|c| c.as_ref()).unwrap_or("0"),
234                "MarginR" => self.margin_r.as_ref().map(|c| c.as_ref()).unwrap_or("0"),
235                "MarginV" => self.margin_v.as_ref().map(|c| c.as_ref()).unwrap_or("0"),
236                "MarginT" => self.margin_t.as_ref().map(|c| c.as_ref()).unwrap_or("0"),
237                "MarginB" => self.margin_b.as_ref().map(|c| c.as_ref()).unwrap_or("0"),
238                "Effect" => self.effect.as_ref().map(|c| c.as_ref()).unwrap_or(""),
239                "Text" => self.text.as_ref().map(|c| c.as_ref()).unwrap_or(""),
240                _ => {
241                    return Err(EditorError::FormatLineError {
242                        message: format!("Unknown event field: {field}"),
243                    })
244                }
245            };
246            field_values.push(value.to_string());
247        }
248
249        // Build the event line
250        let line = format!("{event_type_str}: {}", field_values.join(","));
251        Ok(line)
252    }
253}
254
255/// Builder for creating ASS styles with fluent API
256#[derive(Debug, Default, Clone)]
257pub struct StyleBuilder {
258    name: Option<String>,
259    fontname: Option<String>,
260    fontsize: Option<u32>,
261    primary_colour: Option<String>,
262    secondary_colour: Option<String>,
263    outline_colour: Option<String>,
264    back_colour: Option<String>,
265    bold: Option<bool>,
266    italic: Option<bool>,
267    underline: Option<bool>,
268    strikeout: Option<bool>,
269    scale_x: Option<f32>,
270    scale_y: Option<f32>,
271    spacing: Option<f32>,
272    angle: Option<f32>,
273    border_style: Option<u32>,
274    outline: Option<f32>,
275    shadow: Option<f32>,
276    alignment: Option<u32>,
277    margin_l: Option<u32>,
278    margin_r: Option<u32>,
279    margin_v: Option<u32>,
280    margin_t: Option<u32>,
281    margin_b: Option<u32>,
282    encoding: Option<u32>,
283    alpha_level: Option<u32>,
284    relative_to: Option<String>,
285}
286
287impl StyleBuilder {
288    /// Create a new style builder
289    pub fn new() -> Self {
290        Self::default()
291    }
292
293    /// Create a style builder with default values
294    pub fn default_style() -> Self {
295        Self {
296            fontname: Some("Arial".to_string()),
297            fontsize: Some(20),
298            primary_colour: Some("&Hffffff".to_string()),
299            secondary_colour: Some("&Hff0000".to_string()),
300            outline_colour: Some("&H0".to_string()),
301            back_colour: Some("&H0".to_string()),
302            bold: Some(false),
303            italic: Some(false),
304            underline: Some(false),
305            strikeout: Some(false),
306            scale_x: Some(100.0),
307            scale_y: Some(100.0),
308            spacing: Some(0.0),
309            angle: Some(0.0),
310            border_style: Some(1),
311            outline: Some(2.0),
312            shadow: Some(0.0),
313            alignment: Some(2),
314            margin_l: Some(10),
315            margin_r: Some(10),
316            margin_v: Some(10),
317            encoding: Some(1),
318            ..Self::default()
319        }
320    }
321
322    /// Set style name
323    pub fn name(mut self, name: &str) -> Self {
324        self.name = Some(name.to_string());
325        self
326    }
327
328    /// Set font name
329    pub fn font(mut self, font: &str) -> Self {
330        self.fontname = Some(font.to_string());
331        self
332    }
333
334    /// Set font size
335    pub fn size(mut self, size: u32) -> Self {
336        self.fontsize = Some(size);
337        self
338    }
339
340    /// Set primary text color (in ASS color format)
341    pub fn color(mut self, color: &str) -> Self {
342        self.primary_colour = Some(color.to_string());
343        self
344    }
345
346    /// Set bold formatting
347    pub fn bold(mut self, bold: bool) -> Self {
348        self.bold = Some(bold);
349        self
350    }
351
352    /// Set italic formatting
353    pub fn italic(mut self, italic: bool) -> Self {
354        self.italic = Some(italic);
355        self
356    }
357
358    /// Set alignment (1-9, numpad style)
359    pub fn align(mut self, alignment: u32) -> Self {
360        self.alignment = Some(alignment);
361        self
362    }
363
364    /// Set secondary color (for collision effects)
365    pub fn secondary_color(mut self, color: &str) -> Self {
366        self.secondary_colour = Some(color.to_string());
367        self
368    }
369
370    /// Set outline color
371    pub fn outline_color(mut self, color: &str) -> Self {
372        self.outline_colour = Some(color.to_string());
373        self
374    }
375
376    /// Set shadow/background color
377    pub fn back_color(mut self, color: &str) -> Self {
378        self.back_colour = Some(color.to_string());
379        self
380    }
381
382    /// Set underline formatting
383    pub fn underline(mut self, underline: bool) -> Self {
384        self.underline = Some(underline);
385        self
386    }
387
388    /// Set strikeout formatting
389    pub fn strikeout(mut self, strikeout: bool) -> Self {
390        self.strikeout = Some(strikeout);
391        self
392    }
393
394    /// Set horizontal scale percentage
395    pub fn scale_x(mut self, scale: f32) -> Self {
396        self.scale_x = Some(scale);
397        self
398    }
399
400    /// Set vertical scale percentage
401    pub fn scale_y(mut self, scale: f32) -> Self {
402        self.scale_y = Some(scale);
403        self
404    }
405
406    /// Set character spacing in pixels
407    pub fn spacing(mut self, spacing: f32) -> Self {
408        self.spacing = Some(spacing);
409        self
410    }
411
412    /// Set rotation angle in degrees
413    pub fn angle(mut self, angle: f32) -> Self {
414        self.angle = Some(angle);
415        self
416    }
417
418    /// Set border style (1=outline+shadow, 3=opaque box)
419    pub fn border_style(mut self, style: u32) -> Self {
420        self.border_style = Some(style);
421        self
422    }
423
424    /// Set outline width in pixels
425    pub fn outline(mut self, width: f32) -> Self {
426        self.outline = Some(width);
427        self
428    }
429
430    /// Set shadow depth in pixels
431    pub fn shadow(mut self, depth: f32) -> Self {
432        self.shadow = Some(depth);
433        self
434    }
435
436    /// Set left margin in pixels
437    pub fn margin_left(mut self, margin: u32) -> Self {
438        self.margin_l = Some(margin);
439        self
440    }
441
442    /// Set right margin in pixels
443    pub fn margin_right(mut self, margin: u32) -> Self {
444        self.margin_r = Some(margin);
445        self
446    }
447
448    /// Set vertical margin in pixels
449    pub fn margin_vertical(mut self, margin: u32) -> Self {
450        self.margin_v = Some(margin);
451        self
452    }
453
454    /// Set top margin in pixels (V4++)
455    pub fn margin_top(mut self, margin: u32) -> Self {
456        self.margin_t = Some(margin);
457        self
458    }
459
460    /// Set bottom margin in pixels (V4++)
461    pub fn margin_bottom(mut self, margin: u32) -> Self {
462        self.margin_b = Some(margin);
463        self
464    }
465
466    /// Set font encoding identifier
467    pub fn encoding(mut self, encoding: u32) -> Self {
468        self.encoding = Some(encoding);
469        self
470    }
471
472    /// Set alpha level (SSA v4) - transparency from 0-255 (0=opaque, 255=transparent)
473    pub fn alpha_level(mut self, alpha: u32) -> Self {
474        self.alpha_level = Some(alpha);
475        self
476    }
477
478    /// Set positioning context (V4++)
479    pub fn relative_to(mut self, relative: &str) -> Self {
480        self.relative_to = Some(relative.to_string());
481        self
482    }
483
484    /// Build the style (validates required fields)
485    pub fn build(self) -> Result<String> {
486        let name = self.name.unwrap_or_else(|| "NewStyle".to_string());
487        let fontname = self.fontname.unwrap_or_else(|| "Arial".to_string());
488        let fontsize = self.fontsize.unwrap_or(20);
489        let primary_colour = self
490            .primary_colour
491            .unwrap_or_else(|| "&Hffffff".to_string());
492        let secondary_colour = self
493            .secondary_colour
494            .unwrap_or_else(|| "&Hff0000".to_string());
495        let outline_colour = self.outline_colour.unwrap_or_else(|| "&H0".to_string());
496        let back_colour = self.back_colour.unwrap_or_else(|| "&H0".to_string());
497        let bold = if self.bold.unwrap_or(false) {
498            "-1"
499        } else {
500            "0"
501        };
502        let italic = if self.italic.unwrap_or(false) {
503            "-1"
504        } else {
505            "0"
506        };
507        let underline = if self.underline.unwrap_or(false) {
508            "-1"
509        } else {
510            "0"
511        };
512        let strikeout = if self.strikeout.unwrap_or(false) {
513            "-1"
514        } else {
515            "0"
516        };
517        let scale_x = self.scale_x.unwrap_or(100.0);
518        let scale_y = self.scale_y.unwrap_or(100.0);
519        let spacing = self.spacing.unwrap_or(0.0);
520        let angle = self.angle.unwrap_or(0.0);
521        let border_style = self.border_style.unwrap_or(1);
522        let outline = self.outline.unwrap_or(2.0);
523        let shadow = self.shadow.unwrap_or(0.0);
524        let alignment = self.alignment.unwrap_or(2);
525        let margin_l = self.margin_l.unwrap_or(10);
526        let margin_r = self.margin_r.unwrap_or(10);
527        let margin_v = self.margin_v.unwrap_or(10);
528        let encoding = self.encoding.unwrap_or(1);
529
530        // Handle V4++ fields - margin_t/margin_b override margin_v when present
531        // relative_to is also a V4++ field
532        // Note: The actual format line would determine the field order and presence
533        // For now, we use the standard V4+ format
534
535        // Format as ASS style line
536        let line = format!(
537            "Style: {name},{fontname},{fontsize},{primary_colour},{secondary_colour},{outline_colour},{back_colour},{bold},{italic},{underline},{strikeout},{scale_x},{scale_y},{spacing},{angle},{border_style},{outline},{shadow},{alignment},{margin_l},{margin_r},{margin_v},{encoding}"
538        );
539
540        Ok(line)
541    }
542
543    /// Build the style with a specific version format
544    pub fn build_with_version(self, version: ScriptVersion) -> Result<String> {
545        // Define format based on version
546        let format = match version {
547            ScriptVersion::SsaV4 => {
548                // SSA v4 has fewer fields
549                vec![
550                    "Name",
551                    "Fontname",
552                    "Fontsize",
553                    "PrimaryColour",
554                    "SecondaryColour",
555                    "TertiaryColour",
556                    "BackColour",
557                    "Bold",
558                    "Italic",
559                    "BorderStyle",
560                    "Outline",
561                    "Shadow",
562                    "Alignment",
563                    "MarginL",
564                    "MarginR",
565                    "MarginV",
566                    "AlphaLevel",
567                    "Encoding",
568                ]
569            }
570            ScriptVersion::AssV4 => {
571                // Standard ASS v4 format
572                vec![
573                    "Name",
574                    "Fontname",
575                    "Fontsize",
576                    "PrimaryColour",
577                    "SecondaryColour",
578                    "OutlineColour",
579                    "BackColour",
580                    "Bold",
581                    "Italic",
582                    "Underline",
583                    "StrikeOut",
584                    "ScaleX",
585                    "ScaleY",
586                    "Spacing",
587                    "Angle",
588                    "BorderStyle",
589                    "Outline",
590                    "Shadow",
591                    "Alignment",
592                    "MarginL",
593                    "MarginR",
594                    "MarginV",
595                    "Encoding",
596                ]
597            }
598            ScriptVersion::AssV4Plus => {
599                // ASS v4++ format with additional fields
600                vec![
601                    "Name",
602                    "Fontname",
603                    "Fontsize",
604                    "PrimaryColour",
605                    "SecondaryColour",
606                    "OutlineColour",
607                    "BackColour",
608                    "Bold",
609                    "Italic",
610                    "Underline",
611                    "StrikeOut",
612                    "ScaleX",
613                    "ScaleY",
614                    "Spacing",
615                    "Angle",
616                    "BorderStyle",
617                    "Outline",
618                    "Shadow",
619                    "Alignment",
620                    "MarginL",
621                    "MarginR",
622                    "MarginV",
623                    "MarginT",
624                    "MarginB",
625                    "Encoding",
626                    "RelativeTo",
627                ]
628            }
629        };
630
631        self.build_with_format(&format)
632    }
633
634    /// Build the style with a specific format line
635    /// The format parameter should contain field names like ["Name", "Fontname", "Fontsize", ...]
636    pub fn build_with_format(&self, format: &[&str]) -> Result<String> {
637        if format.is_empty() {
638            return Err(EditorError::FormatLineError {
639                message: "Format line cannot be empty".to_string(),
640            });
641        }
642
643        // Build field values based on format specification
644        let mut field_values = Vec::with_capacity(format.len());
645
646        for field in format {
647            let value = match *field {
648                "Name" => self.name.clone().unwrap_or_else(|| "NewStyle".to_string()),
649                "Fontname" => self.fontname.clone().unwrap_or_else(|| "Arial".to_string()),
650                "Fontsize" => self.fontsize.unwrap_or(20).to_string(),
651                "PrimaryColour" => self
652                    .primary_colour
653                    .clone()
654                    .unwrap_or_else(|| "&Hffffff".to_string()),
655                "SecondaryColour" => self
656                    .secondary_colour
657                    .clone()
658                    .unwrap_or_else(|| "&Hff0000".to_string()),
659                "OutlineColour" | "TertiaryColour" => self
660                    .outline_colour
661                    .clone()
662                    .unwrap_or_else(|| "&H0".to_string()),
663                "BackColour" => self
664                    .back_colour
665                    .clone()
666                    .unwrap_or_else(|| "&H0".to_string()),
667                "Bold" => if self.bold.unwrap_or(false) {
668                    "-1"
669                } else {
670                    "0"
671                }
672                .to_string(),
673                "Italic" => if self.italic.unwrap_or(false) {
674                    "-1"
675                } else {
676                    "0"
677                }
678                .to_string(),
679                "Underline" => if self.underline.unwrap_or(false) {
680                    "-1"
681                } else {
682                    "0"
683                }
684                .to_string(),
685                "Strikeout" | "StrikeOut" => if self.strikeout.unwrap_or(false) {
686                    "-1"
687                } else {
688                    "0"
689                }
690                .to_string(),
691                "ScaleX" => self.scale_x.unwrap_or(100.0).to_string(),
692                "ScaleY" => self.scale_y.unwrap_or(100.0).to_string(),
693                "Spacing" => self.spacing.unwrap_or(0.0).to_string(),
694                "Angle" => self.angle.unwrap_or(0.0).to_string(),
695                "BorderStyle" => self.border_style.unwrap_or(1).to_string(),
696                "Outline" => self.outline.unwrap_or(2.0).to_string(),
697                "Shadow" => self.shadow.unwrap_or(0.0).to_string(),
698                "Alignment" => self.alignment.unwrap_or(2).to_string(),
699                "MarginL" => self.margin_l.unwrap_or(10).to_string(),
700                "MarginR" => self.margin_r.unwrap_or(10).to_string(),
701                "MarginV" => self.margin_v.unwrap_or(10).to_string(),
702                "MarginT" => self.margin_t.unwrap_or(0).to_string(),
703                "MarginB" => self.margin_b.unwrap_or(0).to_string(),
704                "Encoding" => self.encoding.unwrap_or(1).to_string(),
705                "AlphaLevel" => self.alpha_level.unwrap_or(0).to_string(),
706                "RelativeTo" => self.relative_to.clone().unwrap_or_else(|| "0".to_string()),
707                _ => {
708                    return Err(EditorError::FormatLineError {
709                        message: format!("Unknown style field: {field}"),
710                    })
711                }
712            };
713            field_values.push(value);
714        }
715
716        // Build the style line
717        let line = format!("Style: {}", field_values.join(","));
718        Ok(line)
719    }
720}
721
722#[cfg(test)]
723mod tests {
724    use super::*;
725    #[cfg(not(feature = "std"))]
726    use alloc::string::ToString;
727
728    #[test]
729    fn event_builder_dialogue() {
730        let event = EventBuilder::dialogue()
731            .start_time("0:00:05.00")
732            .end_time("0:00:10.00")
733            .speaker("John")
734            .text("Hello world!")
735            .build()
736            .unwrap();
737
738        assert!(event.contains("Dialogue:"));
739        assert!(event.contains("0:00:05.00"));
740        assert!(event.contains("Hello world!"));
741    }
742
743    #[test]
744    fn event_builder_comment() {
745        let event = EventBuilder::comment()
746            .text("This is a comment")
747            .build()
748            .unwrap();
749
750        assert!(event.contains("Comment:"));
751        assert!(event.contains("This is a comment"));
752    }
753
754    #[test]
755    fn style_builder_default() {
756        let style = StyleBuilder::default_style()
757            .name("TestStyle")
758            .font("Comic Sans MS")
759            .size(24)
760            .bold(true)
761            .build()
762            .unwrap();
763
764        assert!(style.contains("Style: TestStyle"));
765        assert!(style.contains("Comic Sans MS"));
766        assert!(style.contains("24"));
767        assert!(style.contains("-1")); // Bold = true
768    }
769
770    #[test]
771    fn style_builder_minimal() {
772        let style = StyleBuilder::new().name("Minimal").build().unwrap();
773
774        assert!(style.contains("Style: Minimal"));
775        assert!(style.contains("Arial")); // Default font
776    }
777
778    #[test]
779    fn event_builder_with_margins() {
780        let event = EventBuilder::dialogue()
781            .start_time("0:00:05.00")
782            .end_time("0:00:10.00")
783            .margin_left(15)
784            .margin_right(20)
785            .margin_vertical(25)
786            .margin_top(30)
787            .margin_bottom(35)
788            .text("Testing margins")
789            .build()
790            .unwrap();
791
792        assert!(event.contains("Dialogue:"));
793        assert!(event.contains("15")); // margin_l
794        assert!(event.contains("20")); // margin_r
795        assert!(event.contains("25")); // margin_v
796                                       // Note: margin_t and margin_b are stored but not in V4+ format output yet
797    }
798
799    #[test]
800    fn style_builder_all_fields() {
801        let style = StyleBuilder::new()
802            .name("Complete")
803            .font("Helvetica")
804            .size(18)
805            .color("&Hffffff")
806            .secondary_color("&H00ff00")
807            .outline_color("&H0000ff")
808            .back_color("&H808080")
809            .bold(true)
810            .italic(false)
811            .underline(true)
812            .strikeout(false)
813            .scale_x(95.5)
814            .scale_y(105.0)
815            .spacing(1.5)
816            .angle(15.0)
817            .border_style(3)
818            .outline(2.5)
819            .shadow(1.0)
820            .align(7)
821            .margin_left(5)
822            .margin_right(15)
823            .margin_vertical(20)
824            .margin_top(25)
825            .margin_bottom(30)
826            .encoding(0)
827            .relative_to("video")
828            .build()
829            .unwrap();
830
831        assert!(style.contains("Style: Complete"));
832        assert!(style.contains("Helvetica"));
833        assert!(style.contains("18"));
834        assert!(style.contains("&Hffffff"));
835        assert!(style.contains("&H00ff00"));
836        assert!(style.contains("&H0000ff"));
837        assert!(style.contains("&H808080"));
838        assert!(style.contains("-1")); // bold = true
839        assert!(style.contains("95.5"));
840        assert!(style.contains("105"));
841        assert!(style.contains("1.5"));
842        assert!(style.contains("15")); // angle
843        assert!(style.contains("3")); // border_style
844        assert!(style.contains("2.5")); // outline
845        assert!(style.contains("7")); // alignment
846                                      // Note: margin_t, margin_b, and relative_to are stored but not in V4+ format output yet
847    }
848
849    #[test]
850    fn event_builder_with_format_v4plus() {
851        let event = EventBuilder::dialogue()
852            .start_time("0:00:05.00")
853            .end_time("0:00:10.00")
854            .style("Main")
855            .layer(1)
856            .text("Test with format")
857            .build_with_format(&[
858                "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginV",
859                "Effect", "Text",
860            ])
861            .unwrap();
862
863        assert_eq!(
864            event,
865            "Dialogue: 1,0:00:05.00,0:00:10.00,Main,,0,0,0,,Test with format"
866        );
867    }
868
869    #[test]
870    fn event_builder_with_format_v4plusplus() {
871        let event = EventBuilder::dialogue()
872            .start_time("0:00:05.00")
873            .end_time("0:00:10.00")
874            .style("Main")
875            .margin_top(5)
876            .margin_bottom(10)
877            .text("V4++ format")
878            .build_with_format(&[
879                "Layer", "Start", "End", "Style", "Name", "MarginL", "MarginR", "MarginT",
880                "MarginB", "Effect", "Text",
881            ])
882            .unwrap();
883
884        assert_eq!(
885            event,
886            "Dialogue: 0,0:00:05.00,0:00:10.00,Main,,0,0,5,10,,V4++ format"
887        );
888    }
889
890    #[test]
891    fn event_builder_with_format_custom() {
892        let event = EventBuilder::comment()
893            .text("Simple comment")
894            .build_with_format(&["Start", "End", "Text"])
895            .unwrap();
896
897        assert_eq!(event, "Comment: 0:00:00.00,0:00:05.00,Simple comment");
898    }
899
900    #[test]
901    fn event_builder_with_format_error() {
902        let result = EventBuilder::dialogue()
903            .text("Test")
904            .build_with_format(&["InvalidField"]);
905
906        assert!(result.is_err());
907        assert!(result
908            .unwrap_err()
909            .to_string()
910            .contains("Unknown event field"));
911    }
912
913    #[test]
914    fn style_builder_with_format_v4plus() {
915        let style = StyleBuilder::new()
916            .name("TestStyle")
917            .font("Arial")
918            .size(20)
919            .build_with_format(&[
920                "Name",
921                "Fontname",
922                "Fontsize",
923                "PrimaryColour",
924                "SecondaryColour",
925                "OutlineColour",
926                "BackColour",
927                "Bold",
928                "Italic",
929                "Underline",
930                "StrikeOut",
931                "ScaleX",
932                "ScaleY",
933                "Spacing",
934                "Angle",
935                "BorderStyle",
936                "Outline",
937                "Shadow",
938                "Alignment",
939                "MarginL",
940                "MarginR",
941                "MarginV",
942                "Encoding",
943            ])
944            .unwrap();
945
946        assert_eq!(style, "Style: TestStyle,Arial,20,&Hffffff,&Hff0000,&H0,&H0,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1");
947    }
948
949    #[test]
950    fn style_builder_with_format_v4plusplus() {
951        let style = StyleBuilder::new()
952            .name("V4++Style")
953            .margin_top(15)
954            .margin_bottom(20)
955            .relative_to("video")
956            .build_with_format(&[
957                "Name",
958                "Fontname",
959                "Fontsize",
960                "PrimaryColour",
961                "SecondaryColour",
962                "OutlineColour",
963                "BackColour",
964                "Bold",
965                "Italic",
966                "Underline",
967                "StrikeOut",
968                "ScaleX",
969                "ScaleY",
970                "Spacing",
971                "Angle",
972                "BorderStyle",
973                "Outline",
974                "Shadow",
975                "Alignment",
976                "MarginL",
977                "MarginR",
978                "MarginT",
979                "MarginB",
980                "Encoding",
981                "RelativeTo",
982            ])
983            .unwrap();
984
985        assert!(style.contains("V4++Style"));
986        assert!(style.contains("15")); // MarginT
987        assert!(style.contains("20")); // MarginB
988        assert!(style.contains("video")); // RelativeTo
989    }
990
991    #[test]
992    fn style_builder_with_format_minimal() {
993        let style = StyleBuilder::new()
994            .name("MinimalStyle")
995            .build_with_format(&["Name", "Fontname", "Fontsize"])
996            .unwrap();
997
998        assert_eq!(style, "Style: MinimalStyle,Arial,20");
999    }
1000
1001    #[test]
1002    fn event_builder_with_script_version() {
1003        // Test building with SSA v4 format
1004        let event_ssa = EventBuilder::dialogue()
1005            .text("SSA Format")
1006            .start_time("0:00:01.00")
1007            .end_time("0:00:03.00")
1008            .build_with_version(ScriptVersion::SsaV4)
1009            .unwrap();
1010        assert!(event_ssa.contains("SSA Format"));
1011
1012        // Test building with ASS v4+ format
1013        let event_ass = EventBuilder::dialogue()
1014            .text("ASS Format")
1015            .build_with_version(ScriptVersion::AssV4Plus)
1016            .unwrap();
1017        assert!(event_ass.contains("ASS Format"));
1018    }
1019
1020    #[test]
1021    fn style_builder_with_script_version() {
1022        // Test building style with SSA v4 format
1023        let style_ssa = StyleBuilder::new()
1024            .name("TestSSA")
1025            .font("Arial")
1026            .size(18)
1027            .build_with_version(ScriptVersion::SsaV4)
1028            .unwrap();
1029        // SSA v4 has TertiaryColour instead of OutlineColour
1030        assert!(style_ssa.contains("TestSSA"));
1031        assert!(style_ssa.contains("Arial"));
1032
1033        // Test building style with ASS v4+ format
1034        let style_ass = StyleBuilder::new()
1035            .name("TestASS")
1036            .font("Verdana")
1037            .size(20)
1038            .build_with_version(ScriptVersion::AssV4Plus)
1039            .unwrap();
1040        assert!(style_ass.contains("TestASS"));
1041        assert!(style_ass.contains("Verdana"));
1042    }
1043}