ass_core/parser/ast/
style.rs

1//! Style AST node for ASS style definitions
2//!
3//! Contains the Style struct representing style definitions from the
4//! [V4+ Styles] section with zero-copy design and style property accessors.
5
6#[cfg(not(feature = "std"))]
7extern crate alloc;
8#[cfg(not(feature = "std"))]
9use alloc::{format, vec::Vec};
10
11use super::Span;
12#[cfg(debug_assertions)]
13use core::ops::Range;
14
15/// Style definition from [V4+ Styles] section
16///
17/// Represents a single style definition that can be referenced by events.
18/// All fields are stored as zero-copy string references to the original
19/// source text for maximum memory efficiency.
20///
21/// # Examples
22///
23/// ```rust
24/// use ass_core::parser::ast::Style;
25///
26/// let style = Style {
27///     name: "Default",
28///     fontname: "Arial",
29///     fontsize: "20",
30///     ..Style::default()
31/// };
32///
33/// assert_eq!(style.name, "Default");
34/// assert_eq!(style.fontname, "Arial");
35/// ```
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct Style<'a> {
38    /// Style name (must be unique within script)
39    pub name: &'a str,
40
41    /// Parent style name for inheritance (None if no inheritance)
42    pub parent: Option<&'a str>,
43
44    /// Font name for text rendering
45    pub fontname: &'a str,
46
47    /// Font size in points
48    pub fontsize: &'a str,
49
50    /// Primary color in BGR format (&HBBGGRR)
51    pub primary_colour: &'a str,
52
53    /// Secondary color for collision effects
54    pub secondary_colour: &'a str,
55
56    /// Outline color
57    pub outline_colour: &'a str,
58
59    /// Shadow/background color
60    pub back_colour: &'a str,
61
62    /// Bold flag (-1/0 or weight)
63    pub bold: &'a str,
64
65    /// Italic flag (0/1)
66    pub italic: &'a str,
67
68    /// Underline flag (0/1)
69    pub underline: &'a str,
70
71    /// Strikeout flag (0/1)
72    pub strikeout: &'a str,
73
74    /// Horizontal scale percentage
75    pub scale_x: &'a str,
76
77    /// Vertical scale percentage
78    pub scale_y: &'a str,
79
80    /// Character spacing in pixels
81    pub spacing: &'a str,
82
83    /// Rotation angle in degrees
84    pub angle: &'a str,
85
86    /// Border style (1=outline+shadow, 3=opaque box)
87    pub border_style: &'a str,
88
89    /// Outline width in pixels
90    pub outline: &'a str,
91
92    /// Shadow depth in pixels
93    pub shadow: &'a str,
94
95    /// Alignment (1-3 + 4/8 for vertical positioning)
96    pub alignment: &'a str,
97
98    /// Left margin in pixels
99    pub margin_l: &'a str,
100
101    /// Right margin in pixels
102    pub margin_r: &'a str,
103
104    /// Vertical margin in pixels (V4+)
105    pub margin_v: &'a str,
106
107    /// Top margin in pixels (V4++)
108    pub margin_t: Option<&'a str>,
109
110    /// Bottom margin in pixels (V4++)
111    pub margin_b: Option<&'a str>,
112
113    /// Font encoding identifier
114    pub encoding: &'a str,
115
116    /// Positioning context (V4++)
117    pub relative_to: Option<&'a str>,
118
119    /// Span in source text where this style is defined
120    pub span: Span,
121}
122
123impl Style<'_> {
124    /// Convert style to ASS string representation
125    ///
126    /// Generates the standard ASS style line format for V4+ styles.
127    /// Uses `margin_v` by default, but will use `margin_t/margin_b` if provided (V4++ format).
128    ///
129    /// # Examples
130    ///
131    /// ```rust
132    /// # use ass_core::parser::ast::Style;
133    /// let style = Style {
134    ///     name: "TestStyle",
135    ///     fontname: "Arial",
136    ///     fontsize: "20",
137    ///     ..Style::default()
138    /// };
139    /// let ass_string = style.to_ass_string();
140    /// assert!(ass_string.starts_with("Style: TestStyle,Arial,20,"));
141    /// ```
142    #[must_use]
143    pub fn to_ass_string(&self) -> alloc::string::String {
144        // Use standard V4+ format by default
145        // TODO: Support custom format lines
146        format!(
147            "Style: {},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}",
148            self.name,
149            self.fontname,
150            self.fontsize,
151            self.primary_colour,
152            self.secondary_colour,
153            self.outline_colour,
154            self.back_colour,
155            self.bold,
156            self.italic,
157            self.underline,
158            self.strikeout,
159            self.scale_x,
160            self.scale_y,
161            self.spacing,
162            self.angle,
163            self.border_style,
164            self.outline,
165            self.shadow,
166            self.alignment,
167            self.margin_l,
168            self.margin_r,
169            self.margin_v,
170            self.encoding
171        )
172    }
173
174    /// Convert style to ASS string with specific format
175    ///
176    /// Generates an ASS style line according to the provided format specification.
177    /// This allows handling both V4+ and V4++ formats, as well as custom formats.
178    ///
179    /// # Arguments
180    ///
181    /// * `format` - Field names in order (e.g., ["Name", "Fontname", "Fontsize", ...])
182    ///
183    /// # Examples
184    ///
185    /// ```rust
186    /// # use ass_core::parser::ast::Style;
187    /// let style = Style {
188    ///     name: "Simple",
189    ///     fontname: "Arial",
190    ///     fontsize: "16",
191    ///     ..Style::default()
192    /// };
193    /// let format = vec!["Name", "Fontname", "Fontsize"];
194    /// assert_eq!(
195    ///     style.to_ass_string_with_format(&format),
196    ///     "Style: Simple,Arial,16"
197    /// );
198    /// ```
199    #[must_use]
200    pub fn to_ass_string_with_format(&self, format: &[&str]) -> alloc::string::String {
201        let mut field_values = Vec::with_capacity(format.len());
202
203        for field in format {
204            let value = match *field {
205                "Name" => self.name,
206                "Fontname" => self.fontname,
207                "Fontsize" => self.fontsize,
208                "PrimaryColour" => self.primary_colour,
209                "SecondaryColour" => self.secondary_colour,
210                "OutlineColour" | "TertiaryColour" => self.outline_colour,
211                "BackColour" => self.back_colour,
212                "Bold" => self.bold,
213                "Italic" => self.italic,
214                "Underline" => self.underline,
215                "Strikeout" | "StrikeOut" => self.strikeout,
216                "ScaleX" => self.scale_x,
217                "ScaleY" => self.scale_y,
218                "Spacing" => self.spacing,
219                "Angle" => self.angle,
220                "BorderStyle" => self.border_style,
221                "Outline" => self.outline,
222                "Shadow" => self.shadow,
223                "Alignment" => self.alignment,
224                "MarginL" => self.margin_l,
225                "MarginR" => self.margin_r,
226                "MarginV" => self.margin_v,
227                "MarginT" => self.margin_t.unwrap_or("0"),
228                "MarginB" => self.margin_b.unwrap_or("0"),
229                "Encoding" => self.encoding,
230                "RelativeTo" => self.relative_to.unwrap_or("0"),
231                _ => "", // Unknown fields default to empty
232            };
233            field_values.push(value);
234        }
235
236        let joined = field_values.join(",");
237        format!("Style: {joined}")
238    }
239
240    /// Validate all spans in this Style reference valid source
241    ///
242    /// Debug helper to ensure zero-copy invariants are maintained.
243    /// Validates that all string references point to memory within
244    /// the specified source range.
245    ///
246    /// Only available in debug builds to avoid performance overhead.
247    #[cfg(debug_assertions)]
248    #[must_use]
249    pub fn validate_spans(&self, source_range: &Range<usize>) -> bool {
250        let spans = [
251            self.name,
252            self.fontname,
253            self.fontsize,
254            self.primary_colour,
255            self.secondary_colour,
256            self.outline_colour,
257            self.back_colour,
258            self.bold,
259            self.italic,
260            self.underline,
261            self.strikeout,
262            self.scale_x,
263            self.scale_y,
264            self.spacing,
265            self.angle,
266            self.border_style,
267            self.outline,
268            self.shadow,
269            self.alignment,
270            self.margin_l,
271            self.margin_r,
272            self.margin_v,
273            self.encoding,
274        ];
275
276        spans.iter().all(|span| {
277            let ptr = span.as_ptr() as usize;
278            source_range.contains(&ptr)
279        })
280    }
281}
282
283impl Default for Style<'_> {
284    /// Create default ASS style with standard values
285    ///
286    /// Provides the standard ASS default style values as defined
287    /// in the ASS specification for maximum compatibility.
288    fn default() -> Self {
289        Self {
290            name: "Default",
291            parent: None,
292            fontname: "Arial",
293            fontsize: "20",
294            primary_colour: "&Hffffff",
295            secondary_colour: "&H0000ff",
296            outline_colour: "&H000000",
297            back_colour: "&H000000",
298            bold: "0",
299            italic: "0",
300            underline: "0",
301            strikeout: "0",
302            scale_x: "100",
303            scale_y: "100",
304            spacing: "0",
305            angle: "0",
306            border_style: "1",
307            outline: "0",
308            shadow: "0",
309            alignment: "2",
310            margin_l: "10",
311            margin_r: "10",
312            margin_v: "10",
313            margin_t: None,
314            margin_b: None,
315            encoding: "1",
316            relative_to: None,
317            span: Span::new(0, 0, 0, 0),
318        }
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325    #[cfg(not(feature = "std"))]
326    use alloc::string::String;
327    #[cfg(not(feature = "std"))]
328    use alloc::vec;
329
330    #[test]
331    fn style_default_values() {
332        let style = Style::default();
333        assert_eq!(style.name, "Default");
334        assert_eq!(style.fontname, "Arial");
335        assert_eq!(style.fontsize, "20");
336        assert_eq!(style.primary_colour, "&Hffffff");
337        assert_eq!(style.alignment, "2");
338    }
339
340    #[test]
341    fn style_default_all_fields() {
342        let style = Style::default();
343
344        // Test all default field values
345        assert_eq!(style.name, "Default");
346        assert_eq!(style.fontname, "Arial");
347        assert_eq!(style.fontsize, "20");
348        assert_eq!(style.primary_colour, "&Hffffff");
349        assert_eq!(style.secondary_colour, "&H0000ff");
350        assert_eq!(style.outline_colour, "&H000000");
351        assert_eq!(style.back_colour, "&H000000");
352        assert_eq!(style.bold, "0");
353        assert_eq!(style.italic, "0");
354        assert_eq!(style.underline, "0");
355        assert_eq!(style.strikeout, "0");
356        assert_eq!(style.scale_x, "100");
357        assert_eq!(style.scale_y, "100");
358        assert_eq!(style.spacing, "0");
359        assert_eq!(style.angle, "0");
360        assert_eq!(style.border_style, "1");
361        assert_eq!(style.outline, "0");
362        assert_eq!(style.shadow, "0");
363        assert_eq!(style.alignment, "2");
364        assert_eq!(style.margin_l, "10");
365        assert_eq!(style.margin_r, "10");
366        assert_eq!(style.margin_v, "10");
367        assert_eq!(style.encoding, "1");
368    }
369
370    #[test]
371    fn style_clone() {
372        let style = Style::default();
373        let cloned = style.clone();
374        assert_eq!(style, cloned);
375    }
376
377    #[test]
378    fn style_clone_custom() {
379        let style = Style {
380            name: "CustomStyle",
381            parent: None,
382            fontname: "Times New Roman",
383            fontsize: "18",
384            primary_colour: "&H00ff00",
385            secondary_colour: "&Hff0000",
386            outline_colour: "&H0000ff",
387            back_colour: "&H808080",
388            bold: "1",
389            italic: "1",
390            underline: "0",
391            strikeout: "1",
392            scale_x: "110",
393            scale_y: "90",
394            spacing: "2",
395            angle: "15",
396            border_style: "3",
397            outline: "2",
398            shadow: "1",
399            alignment: "5",
400            margin_l: "15",
401            margin_r: "25",
402            margin_v: "20",
403            margin_t: None,
404            margin_b: None,
405            encoding: "0",
406            relative_to: None,
407            span: Span::new(0, 0, 0, 0),
408        };
409
410        let cloned = style.clone();
411        assert_eq!(style, cloned);
412        assert_eq!(cloned.name, "CustomStyle");
413        assert_eq!(cloned.fontname, "Times New Roman");
414        assert_eq!(cloned.bold, "1");
415    }
416
417    #[test]
418    fn style_debug() {
419        let style = Style::default();
420        let debug_str = format!("{style:?}");
421        assert!(debug_str.contains("Style"));
422        assert!(debug_str.contains("Default"));
423    }
424
425    #[test]
426    fn style_debug_custom() {
427        let style = Style {
428            name: "DebugTest",
429            parent: None,
430            fontname: "Helvetica",
431            fontsize: "18",
432            ..Style::default()
433        };
434
435        let debug_str = format!("{style:?}");
436        assert!(debug_str.contains("Style"));
437        assert!(debug_str.contains("DebugTest"));
438        assert!(debug_str.contains("Helvetica"));
439        assert!(debug_str.contains("18"));
440    }
441
442    #[test]
443    fn style_partial_eq() {
444        let style1 = Style::default();
445        let style2 = Style::default();
446        assert_eq!(style1, style2);
447
448        let style3 = Style {
449            name: "Custom",
450            parent: None,
451            ..Style::default()
452        };
453        assert_ne!(style1, style3);
454    }
455
456    #[test]
457    fn style_partial_eq_different_fields() {
458        let base = Style::default();
459
460        // Test inequality with different fields
461        let name_diff = Style {
462            name: "Different",
463            parent: None,
464            ..Style::default()
465        };
466        assert_ne!(base, name_diff);
467
468        let font_diff = Style {
469            fontname: "Comic Sans",
470            parent: None,
471            ..Style::default()
472        };
473        assert_ne!(base, font_diff);
474
475        let size_diff = Style {
476            fontsize: "24",
477            parent: None,
478            ..Style::default()
479        };
480        assert_ne!(base, size_diff);
481
482        let color_diff = Style {
483            primary_colour: "&H00ff00",
484            parent: None,
485            ..Style::default()
486        };
487        assert_ne!(base, color_diff);
488    }
489
490    #[test]
491    fn style_field_access() {
492        let style = Style {
493            name: "TestStyle",
494            parent: None,
495            fontname: "Comic Sans",
496            fontsize: "24",
497            bold: "1",
498            italic: "1",
499            ..Style::default()
500        };
501
502        assert_eq!(style.name, "TestStyle");
503        assert_eq!(style.fontname, "Comic Sans");
504        assert_eq!(style.fontsize, "24");
505        assert_eq!(style.bold, "1");
506        assert_eq!(style.italic, "1");
507    }
508
509    #[test]
510    fn style_field_access_comprehensive() {
511        let style = Style {
512            name: "ComprehensiveTest",
513            parent: None,
514            fontname: "Verdana",
515            fontsize: "14",
516            primary_colour: "&Hff0000",
517            secondary_colour: "&H00ff00",
518            outline_colour: "&H0000ff",
519            back_colour: "&Hffffff",
520            bold: "-1",
521            italic: "1",
522            underline: "1",
523            strikeout: "0",
524            scale_x: "125",
525            scale_y: "75",
526            spacing: "3",
527            angle: "45",
528            border_style: "3",
529            outline: "3",
530            shadow: "2",
531            alignment: "9",
532            margin_l: "20",
533            margin_r: "30",
534            margin_v: "15",
535            margin_t: None,
536            margin_b: None,
537            encoding: "2",
538            relative_to: None,
539            span: Span::new(0, 0, 0, 0),
540        };
541
542        // Test all field accesses
543        assert_eq!(style.name, "ComprehensiveTest");
544        assert_eq!(style.fontname, "Verdana");
545        assert_eq!(style.fontsize, "14");
546        assert_eq!(style.primary_colour, "&Hff0000");
547        assert_eq!(style.secondary_colour, "&H00ff00");
548        assert_eq!(style.outline_colour, "&H0000ff");
549        assert_eq!(style.back_colour, "&Hffffff");
550        assert_eq!(style.bold, "-1");
551        assert_eq!(style.italic, "1");
552        assert_eq!(style.underline, "1");
553        assert_eq!(style.strikeout, "0");
554        assert_eq!(style.scale_x, "125");
555        assert_eq!(style.scale_y, "75");
556        assert_eq!(style.spacing, "3");
557        assert_eq!(style.angle, "45");
558        assert_eq!(style.border_style, "3");
559        assert_eq!(style.outline, "3");
560        assert_eq!(style.shadow, "2");
561        assert_eq!(style.alignment, "9");
562        assert_eq!(style.margin_l, "20");
563        assert_eq!(style.margin_r, "30");
564        assert_eq!(style.margin_v, "15");
565        assert_eq!(style.encoding, "2");
566    }
567
568    #[test]
569    fn style_empty_strings() {
570        let style = Style {
571            name: "",
572            parent: None,
573            fontname: "",
574            fontsize: "",
575            primary_colour: "",
576            secondary_colour: "",
577            outline_colour: "",
578            back_colour: "",
579            bold: "",
580            italic: "",
581            underline: "",
582            strikeout: "",
583            scale_x: "",
584            scale_y: "",
585            spacing: "",
586            angle: "",
587            border_style: "",
588            outline: "",
589            shadow: "",
590            alignment: "",
591            margin_l: "",
592            margin_r: "",
593            margin_v: "",
594            margin_t: None,
595            margin_b: None,
596            encoding: "",
597            relative_to: None,
598            span: Span::new(0, 0, 0, 0),
599        };
600
601        // All fields should be empty strings
602        assert_eq!(style.name, "");
603        assert_eq!(style.fontname, "");
604        assert_eq!(style.fontsize, "");
605        assert_eq!(style.primary_colour, "");
606        assert_eq!(style.alignment, "");
607        assert_eq!(style.encoding, "");
608    }
609
610    #[cfg(debug_assertions)]
611    #[test]
612    fn style_validate_spans() {
613        let source = "Default,Arial,20,&Hffffff,&H0000ff,&H000000,&H000000,0,0,0,0,100,100,0,0,1,0,0,2,10,10,10,1";
614        let source_start = source.as_ptr() as usize;
615        let source_end = source_start + source.len();
616        let source_range = source_start..source_end;
617
618        // Parse all fields from the source to ensure all references are within range
619        let fields: Vec<&str> = source.split(',').collect();
620        assert_eq!(fields.len(), 23); // ASS v4+ style has 23 fields
621
622        // Create style with all references within the source range
623        let style = Style {
624            name: fields[0],
625            parent: None,
626            fontname: fields[1],
627            fontsize: fields[2],
628            primary_colour: fields[3],
629            secondary_colour: fields[4],
630            outline_colour: fields[5],
631            back_colour: fields[6],
632            bold: fields[7],
633            italic: fields[8],
634            underline: fields[9],
635            strikeout: fields[10],
636            scale_x: fields[11],
637            scale_y: fields[12],
638            spacing: fields[13],
639            angle: fields[14],
640            border_style: fields[15],
641            outline: fields[16],
642            shadow: fields[17],
643            alignment: fields[18],
644            margin_l: fields[19],
645            margin_r: fields[20],
646            margin_v: fields[21],
647            margin_t: None,
648            margin_b: None,
649            encoding: fields[22],
650            relative_to: None,
651            span: Span::new(0, 0, 0, 0),
652        };
653
654        // Actually call the validate_spans method
655        assert!(style.validate_spans(&source_range));
656
657        // Verify the fields are correct
658        assert_eq!(style.name, "Default");
659        assert_eq!(style.fontname, "Arial");
660        assert_eq!(style.fontsize, "20");
661    }
662
663    #[cfg(debug_assertions)]
664    #[test]
665    fn style_validate_spans_invalid() {
666        let source1 = "Default,Arial,20";
667        let source2 = "Other,Times,16";
668        let source1_start = source1.as_ptr() as usize;
669        let source1_end = source1_start + source1.len();
670        let source1_range = source1_start..source1_end;
671
672        // Create style with references from different source
673        let style = Style {
674            name: &source2[0..5], // "Other" - from different source
675            parent: None,
676            fontname: &source1[8..13],  // "Arial" - from source1
677            fontsize: &source1[14..16], // "20" - from source1
678            ..Style::default()
679        };
680
681        // This should fail since name is from different source
682        assert!(!style.validate_spans(&source1_range));
683    }
684
685    #[test]
686    fn style_lifetimes() {
687        let source = String::from("TestStyle,Times,16");
688        let style = {
689            let parts: Vec<&str> = source.split(',').collect();
690            Style {
691                name: parts[0],
692                parent: None,
693                fontname: parts[1],
694                fontsize: parts[2],
695                ..Style::default()
696            }
697        };
698
699        assert_eq!(style.name, "TestStyle");
700        assert_eq!(style.fontname, "Times");
701        assert_eq!(style.fontsize, "16");
702    }
703
704    #[test]
705    fn style_equality_all_combinations() {
706        let style1 = Style::default();
707        let mut style2 = Style::default();
708
709        // Should be equal initially
710        assert_eq!(style1, style2);
711
712        // Test each field for inequality
713        style2.name = "Different";
714        assert_ne!(style1, style2);
715        style2 = Style::default();
716
717        style2.fontname = "Different";
718        assert_ne!(style1, style2);
719        style2 = Style::default();
720
721        style2.fontsize = "Different";
722        assert_ne!(style1, style2);
723        style2 = Style::default();
724
725        style2.primary_colour = "Different";
726        assert_ne!(style1, style2);
727        style2 = Style::default();
728
729        style2.secondary_colour = "Different";
730        assert_ne!(style1, style2);
731        style2 = Style::default();
732
733        style2.outline_colour = "Different";
734        assert_ne!(style1, style2);
735        style2 = Style::default();
736
737        style2.back_colour = "Different";
738        assert_ne!(style1, style2);
739        style2 = Style::default();
740
741        style2.bold = "Different";
742        assert_ne!(style1, style2);
743        style2 = Style::default();
744
745        style2.italic = "Different";
746        assert_ne!(style1, style2);
747        style2 = Style::default();
748
749        style2.underline = "Different";
750        assert_ne!(style1, style2);
751        style2 = Style::default();
752
753        style2.strikeout = "Different";
754        assert_ne!(style1, style2);
755        style2 = Style::default();
756
757        style2.scale_x = "Different";
758        assert_ne!(style1, style2);
759        style2 = Style::default();
760
761        style2.scale_y = "Different";
762        assert_ne!(style1, style2);
763        style2 = Style::default();
764
765        style2.spacing = "Different";
766        assert_ne!(style1, style2);
767        style2 = Style::default();
768
769        style2.angle = "Different";
770        assert_ne!(style1, style2);
771        style2 = Style::default();
772
773        style2.border_style = "Different";
774        assert_ne!(style1, style2);
775        style2 = Style::default();
776
777        style2.outline = "Different";
778        assert_ne!(style1, style2);
779        style2 = Style::default();
780
781        style2.shadow = "Different";
782        assert_ne!(style1, style2);
783        style2 = Style::default();
784
785        style2.alignment = "Different";
786        assert_ne!(style1, style2);
787        style2 = Style::default();
788
789        style2.margin_l = "Different";
790        assert_ne!(style1, style2);
791        style2 = Style::default();
792
793        style2.margin_r = "Different";
794        assert_ne!(style1, style2);
795        style2 = Style::default();
796
797        style2.margin_v = "Different";
798        assert_ne!(style1, style2);
799        style2 = Style::default();
800
801        style2.encoding = "Different";
802        assert_ne!(style1, style2);
803    }
804
805    #[test]
806    fn style_default_construction() {
807        // Test that Default::default() works correctly
808        let style: Style = Style::default();
809        assert_eq!(style.name, "Default");
810        assert_eq!(style.fontname, "Arial");
811        assert_eq!(style.fontsize, "20");
812        assert_eq!(style.primary_colour, "&Hffffff");
813        assert_eq!(style.secondary_colour, "&H0000ff");
814        assert_eq!(style.outline_colour, "&H000000");
815        assert_eq!(style.back_colour, "&H000000");
816        assert_eq!(style.bold, "0");
817        assert_eq!(style.italic, "0");
818        assert_eq!(style.underline, "0");
819        assert_eq!(style.strikeout, "0");
820        assert_eq!(style.scale_x, "100");
821        assert_eq!(style.scale_y, "100");
822        assert_eq!(style.spacing, "0");
823        assert_eq!(style.angle, "0");
824        assert_eq!(style.border_style, "1");
825        assert_eq!(style.outline, "0");
826        assert_eq!(style.shadow, "0");
827        assert_eq!(style.alignment, "2");
828        assert_eq!(style.margin_l, "10");
829        assert_eq!(style.margin_r, "10");
830        assert_eq!(style.margin_v, "10");
831        assert_eq!(style.encoding, "1");
832    }
833
834    #[test]
835    fn style_struct_creation() {
836        // Test direct struct creation syntax
837        let style = Style {
838            name: "TestName",
839            parent: None,
840            fontname: "TestFont",
841            fontsize: "12",
842            primary_colour: "&H123456",
843            secondary_colour: "&H654321",
844            outline_colour: "&Habcdef",
845            back_colour: "&Hfedcba",
846            bold: "1",
847            italic: "1",
848            underline: "1",
849            strikeout: "1",
850            scale_x: "150",
851            scale_y: "75",
852            spacing: "5",
853            angle: "90",
854            border_style: "2",
855            outline: "1",
856            shadow: "3",
857            alignment: "5",
858            margin_l: "25",
859            margin_r: "35",
860            margin_v: "20",
861            margin_t: None,
862            margin_b: None,
863            encoding: "3",
864            relative_to: None,
865            span: Span::new(0, 0, 0, 0),
866        };
867
868        // Verify all fields are set correctly
869        assert_eq!(style.name, "TestName");
870        assert_eq!(style.fontname, "TestFont");
871        assert_eq!(style.fontsize, "12");
872        assert_eq!(style.primary_colour, "&H123456");
873        assert_eq!(style.secondary_colour, "&H654321");
874        assert_eq!(style.outline_colour, "&Habcdef");
875        assert_eq!(style.back_colour, "&Hfedcba");
876        assert_eq!(style.bold, "1");
877        assert_eq!(style.italic, "1");
878        assert_eq!(style.underline, "1");
879        assert_eq!(style.strikeout, "1");
880        assert_eq!(style.scale_x, "150");
881        assert_eq!(style.scale_y, "75");
882        assert_eq!(style.spacing, "5");
883        assert_eq!(style.angle, "90");
884        assert_eq!(style.border_style, "2");
885        assert_eq!(style.outline, "1");
886        assert_eq!(style.shadow, "3");
887        assert_eq!(style.alignment, "5");
888        assert_eq!(style.margin_l, "25");
889        assert_eq!(style.margin_r, "35");
890        assert_eq!(style.margin_v, "20");
891        assert_eq!(style.encoding, "3");
892    }
893
894    #[test]
895    fn style_mix_default_and_custom() {
896        // Test struct update syntax with defaults
897        let style = Style {
898            name: "MixedStyle",
899            parent: None,
900            fontsize: "22",
901            bold: "1",
902            italic: "1",
903            primary_colour: "&Hff00ff",
904            alignment: "7",
905            ..Style::default()
906        };
907
908        // Custom fields
909        assert_eq!(style.name, "MixedStyle");
910        assert_eq!(style.fontsize, "22");
911        assert_eq!(style.bold, "1");
912        assert_eq!(style.italic, "1");
913        assert_eq!(style.primary_colour, "&Hff00ff");
914        assert_eq!(style.alignment, "7");
915
916        // Default fields
917        assert_eq!(style.fontname, "Arial");
918        assert_eq!(style.underline, "0");
919        assert_eq!(style.strikeout, "0");
920        assert_eq!(style.scale_x, "100");
921        assert_eq!(style.encoding, "1");
922    }
923
924    #[test]
925    fn style_to_ass_string() {
926        let style = Style::default();
927        let ass_string = style.to_ass_string();
928
929        assert_eq!(
930            ass_string,
931            "Style: Default,Arial,20,&Hffffff,&H0000ff,&H000000,&H000000,0,0,0,0,100,100,0,0,1,0,0,2,10,10,10,1"
932        );
933    }
934
935    #[test]
936    fn style_to_ass_string_custom() {
937        let style = Style {
938            name: "Custom",
939            fontname: "Times New Roman",
940            fontsize: "24",
941            primary_colour: "&H00ff00",
942            bold: "-1",
943            italic: "1",
944            scale_x: "95",
945            scale_y: "105",
946            alignment: "5",
947            ..Style::default()
948        };
949
950        let ass_string = style.to_ass_string();
951        assert!(ass_string.contains("Custom,Times New Roman,24"));
952        assert!(ass_string.contains("&H00ff00"));
953        assert!(ass_string.contains("-1,1")); // bold, italic
954        assert!(ass_string.contains("95,105")); // scale_x, scale_y
955        assert!(ass_string.contains(",5,")); // alignment
956    }
957
958    #[test]
959    fn style_to_ass_string_with_format() {
960        let style = Style {
961            name: "TestStyle",
962            fontname: "Arial",
963            fontsize: "20",
964            ..Style::default()
965        };
966
967        // V4+ standard format
968        let v4_format = vec![
969            "Name",
970            "Fontname",
971            "Fontsize",
972            "PrimaryColour",
973            "SecondaryColour",
974            "OutlineColour",
975            "BackColour",
976            "Bold",
977            "Italic",
978            "Underline",
979            "StrikeOut",
980            "ScaleX",
981            "ScaleY",
982            "Spacing",
983            "Angle",
984            "BorderStyle",
985            "Outline",
986            "Shadow",
987            "Alignment",
988            "MarginL",
989            "MarginR",
990            "MarginV",
991            "Encoding",
992        ];
993        let v4_string = style.to_ass_string_with_format(&v4_format);
994        assert_eq!(
995            v4_string,
996            "Style: TestStyle,Arial,20,&Hffffff,&H0000ff,&H000000,&H000000,0,0,0,0,100,100,0,0,1,0,0,2,10,10,10,1"
997        );
998
999        // Minimal format
1000        let min_format = vec!["Name", "Fontname", "Fontsize"];
1001        let min_string = style.to_ass_string_with_format(&min_format);
1002        assert_eq!(min_string, "Style: TestStyle,Arial,20");
1003
1004        // V4++ format with margin_t/margin_b
1005        let style_v4pp = Style {
1006            name: "V4++Style",
1007            margin_t: Some("15"),
1008            margin_b: Some("20"),
1009            relative_to: Some("video"),
1010            ..Style::default()
1011        };
1012        let v4pp_format = vec![
1013            "Name",
1014            "Fontname",
1015            "Fontsize",
1016            "PrimaryColour",
1017            "SecondaryColour",
1018            "OutlineColour",
1019            "BackColour",
1020            "Bold",
1021            "Italic",
1022            "Underline",
1023            "StrikeOut",
1024            "ScaleX",
1025            "ScaleY",
1026            "Spacing",
1027            "Angle",
1028            "BorderStyle",
1029            "Outline",
1030            "Shadow",
1031            "Alignment",
1032            "MarginL",
1033            "MarginR",
1034            "MarginT",
1035            "MarginB",
1036            "Encoding",
1037            "RelativeTo",
1038        ];
1039        let v4pp_string = style_v4pp.to_ass_string_with_format(&v4pp_format);
1040        assert!(v4pp_string.contains("V4++Style"));
1041        assert!(v4pp_string.contains(",15,20,")); // margin_t, margin_b
1042        assert!(v4pp_string.contains(",video")); // relative_to
1043    }
1044
1045    #[cfg(debug_assertions)]
1046    #[test]
1047    fn style_validate_spans_comprehensive() {
1048        let source = "Name,Font,Size,Primary,Secondary,Outline,Back,Bold,Italic,Under,Strike,ScX,ScY,Sp,Ang,Border,Out,Shad,Align,ML,MR,MV,Enc";
1049        let source_start = source.as_ptr() as usize;
1050        let source_end = source_start + source.len();
1051        let source_range = source_start..source_end;
1052
1053        let fields: Vec<&str> = source.split(',').collect();
1054        let style = Style {
1055            name: fields[0],
1056            parent: None,
1057            fontname: fields[1],
1058            fontsize: fields[2],
1059            primary_colour: fields[3],
1060            secondary_colour: fields[4],
1061            outline_colour: fields[5],
1062            back_colour: fields[6],
1063            bold: fields[7],
1064            italic: fields[8],
1065            underline: fields[9],
1066            strikeout: fields[10],
1067            scale_x: fields[11],
1068            scale_y: fields[12],
1069            spacing: fields[13],
1070            angle: fields[14],
1071            border_style: fields[15],
1072            outline: fields[16],
1073            shadow: fields[17],
1074            alignment: fields[18],
1075            margin_l: fields[19],
1076            margin_r: fields[20],
1077            margin_v: fields[21],
1078            margin_t: None,
1079            margin_b: None,
1080            encoding: fields[22],
1081            relative_to: None,
1082            span: Span::new(0, 0, 0, 0),
1083        };
1084
1085        // Should validate successfully since all fields are from source
1086        assert!(style.validate_spans(&source_range));
1087    }
1088}