subx_cli/core/formats/
mod.rs

1//! Comprehensive subtitle format handling and conversion system.
2//!
3//! This module provides a unified interface for parsing, converting, and managing
4//! multiple subtitle formats including SRT, ASS/SSA, VTT (WebVTT), and SUB formats.
5//! It handles format detection, parsing, conversion between formats, and preservation
6//! of styling information where supported.
7//!
8//! # Supported Formats
9//!
10//! - **SRT (SubRip)**: The most common subtitle format with simple timing and text
11//! - **ASS/SSA (Advanced SubStation Alpha)**: Advanced format with rich styling support
12//! - **VTT (WebVTT)**: Web-based format with positioning and styling capabilities
13//! - **SUB (MicroDVD)**: Frame-based timing format
14//!
15//! # Architecture
16//!
17//! The format system is built around several key components:
18//!
19//! - **Format Detection**: Automatic detection based on file extension and content analysis
20//! - **Unified Data Model**: Common `Subtitle` and `SubtitleEntry` structures for all formats
21//! - **Format-Specific Parsers**: Dedicated parsing logic for each format
22//! - **Conversion Engine**: Intelligent conversion between formats with feature mapping
23//! - **Styling Preservation**: Maintains formatting information during conversions
24//! - **Encoding Handling**: Automatic encoding detection and conversion
25//!
26//! # Usage Examples
27//!
28//! ## Basic Format Detection and Parsing
29//!
30//! ```rust,ignore
31//! use subx_cli::core::formats::{manager::FormatManager, SubtitleFormatType};
32//! use std::path::Path;
33//!
34//! // Create format manager
35//! let manager = FormatManager::new();
36//!
37//! // Detect format from file
38//! let format = manager.detect_format(Path::new("movie.srt"))?;
39//! println!("Detected format: {}", format);
40//!
41//! // Parse subtitle file
42//! let subtitle = manager.parse_file(Path::new("movie.srt"))?;
43//! println!("Loaded {} entries", subtitle.entries.len());
44//! ```
45//!
46//! ## Format Conversion
47//!
48//! ```rust,ignore
49//! use subx_cli::core::formats::converter::FormatConverter;
50//!
51//! let converter = FormatConverter::new();
52//!
53//! // Convert SRT to ASS format
54//! let ass_content = converter.convert(
55//!     &srt_subtitle,
56//!     SubtitleFormatType::Ass,
57//!     None // Use default conversion options
58//! )?;
59//!
60//! // Save converted content
61//! std::fs::write("movie.ass", ass_content)?;
62//! ```
63//!
64//! ## Working with Styling Information
65//!
66//! ```rust,ignore
67//! use subx_cli::core::formats::{StylingInfo, SubtitleEntry};
68//! use std::time::Duration;
69//!
70//! // Create a styled subtitle entry
71//! let styled_entry = SubtitleEntry {
72//!     index: 1,
73//!     start_time: Duration::from_secs(10),
74//!     end_time: Duration::from_secs(13),
75//!     text: "Styled subtitle text".to_string(),
76//!     styling: Some(StylingInfo {
77//!         font_name: Some("Arial".to_string()),
78//!         font_size: Some(20),
79//!         color: Some("#FFFFFF".to_string()),
80//!         bold: true,
81//!         italic: false,
82//!         underline: false,
83//!     }),
84//! };
85//! ```
86//!
87//! # Format-Specific Features
88//!
89//! ## SRT Format
90//! - Simple timing format (hours:minutes:seconds,milliseconds)
91//! - Basic text formatting with HTML-like tags
92//! - Wide compatibility across media players
93//!
94//! ## ASS/SSA Format
95//! - Advanced styling with fonts, colors, positioning
96//! - Animation and transition effects
97//! - Karaoke timing support
98//! - Multiple style definitions
99//!
100//! ## VTT Format
101//! - Web-optimized format for HTML5 video
102//! - CSS-based styling support
103//! - Positioning and region definitions
104//! - Metadata and chapter support
105//!
106//! ## SUB Format
107//! - Frame-based timing (requires frame rate)
108//! - Simple text format
109//! - Legacy format support
110//!
111//! # Error Handling
112//!
113//! The format system provides comprehensive error handling for:
114//! - Invalid file formats or corrupted content
115//! - Encoding detection and conversion failures
116//! - Timing inconsistencies and overlaps
117//! - Missing or invalid styling information
118//! - File I/O errors during parsing or saving
119//!
120//! # Performance Considerations
121//!
122//! - **Streaming Parsing**: Large files are processed incrementally
123//! - **Memory Efficiency**: Minimal memory footprint for subtitle data
124//! - **Caching**: Format detection results are cached for performance
125//! - **Parallel Processing**: Multiple files can be processed concurrently
126//!
127//! # Thread Safety
128//!
129//! All format operations are thread-safe and can be used in concurrent environments.
130//! The format manager and converters can be safely shared across threads.
131
132#![allow(dead_code)]
133
134pub mod ass;
135pub mod converter;
136pub mod encoding;
137pub mod manager;
138/// SubRip Text (.srt) subtitle format support
139pub mod srt;
140pub mod styling;
141pub mod sub;
142pub mod transformers;
143pub mod vtt;
144
145use std::time::Duration;
146
147/// Supported subtitle format types with their characteristics and use cases.
148///
149/// This enum represents the different subtitle formats that SubX can process,
150/// each with distinct features, compatibility, and use cases.
151///
152/// # Format Characteristics
153///
154/// - **SRT**: Universal compatibility, simple timing, basic formatting
155/// - **ASS**: Advanced styling, animations, precise positioning
156/// - **VTT**: Web-optimized, CSS styling, HTML5 video integration
157/// - **SUB**: Frame-based timing, legacy format support
158///
159/// # Examples
160///
161/// ```rust
162/// use subx_cli::core::formats::SubtitleFormatType;
163///
164/// let format = SubtitleFormatType::Srt;
165/// assert_eq!(format.as_str(), "srt");
166/// assert_eq!(format.to_string(), "srt");
167///
168/// // Check format capabilities
169/// assert!(format.supports_basic_timing());
170/// assert!(!format.supports_advanced_styling());
171/// ```
172#[derive(Debug, Clone, PartialEq, Eq)]
173pub enum SubtitleFormatType {
174    /// SubRip Text format (.srt) - Most common subtitle format.
175    ///
176    /// Features:
177    /// - Simple timing format (HH:MM:SS,mmm)
178    /// - Basic HTML-like formatting tags
179    /// - Universal player compatibility
180    /// - Lightweight and fast parsing
181    Srt,
182
183    /// Advanced SubStation Alpha format (.ass/.ssa) - Professional subtitling format.
184    ///
185    /// Features:
186    /// - Rich styling with fonts, colors, effects
187    /// - Precise positioning and alignment
188    /// - Animation and transition support
189    /// - Karaoke timing capabilities
190    /// - Multiple style definitions
191    Ass,
192
193    /// WebVTT format (.vtt) - Web-optimized subtitle format.
194    ///
195    /// Features:
196    /// - CSS-based styling
197    /// - Positioning and region support
198    /// - Metadata and chapter markers
199    /// - HTML5 video integration
200    /// - Web accessibility features
201    Vtt,
202
203    /// MicroDVD format (.sub) - Frame-based subtitle format.
204    ///
205    /// Features:
206    /// - Frame-based timing (requires FPS)
207    /// - Simple text format
208    /// - Legacy format support
209    /// - Compact file size
210    Sub,
211}
212
213impl SubtitleFormatType {
214    /// Get the format as a lowercase string slice (e.g., "srt").
215    ///
216    /// This method returns the standard file extension for the format,
217    /// which can be used for file naming and format identification.
218    ///
219    /// # Examples
220    ///
221    /// ```rust
222    /// use subx_cli::core::formats::SubtitleFormatType;
223    ///
224    /// assert_eq!(SubtitleFormatType::Srt.as_str(), "srt");
225    /// assert_eq!(SubtitleFormatType::Ass.as_str(), "ass");
226    /// assert_eq!(SubtitleFormatType::Vtt.as_str(), "vtt");
227    /// assert_eq!(SubtitleFormatType::Sub.as_str(), "sub");
228    /// ```
229    pub fn as_str(&self) -> &'static str {
230        match self {
231            SubtitleFormatType::Srt => "srt",
232            SubtitleFormatType::Ass => "ass",
233            SubtitleFormatType::Vtt => "vtt",
234            SubtitleFormatType::Sub => "sub",
235        }
236    }
237
238    /// Check if the format supports basic timing information.
239    ///
240    /// All supported formats have basic timing capabilities.
241    ///
242    /// # Returns
243    ///
244    /// Always returns `true` for all current formats.
245    pub fn supports_basic_timing(&self) -> bool {
246        true
247    }
248
249    /// Check if the format supports advanced styling features.
250    ///
251    /// Advanced styling includes fonts, colors, positioning, and effects.
252    ///
253    /// # Returns
254    ///
255    /// - `true` for ASS and VTT formats
256    /// - `false` for SRT and SUB formats
257    ///
258    /// # Examples
259    ///
260    /// ```rust
261    /// use subx_cli::core::formats::SubtitleFormatType;
262    ///
263    /// assert!(SubtitleFormatType::Ass.supports_advanced_styling());
264    /// assert!(SubtitleFormatType::Vtt.supports_advanced_styling());
265    /// assert!(!SubtitleFormatType::Srt.supports_advanced_styling());
266    /// assert!(!SubtitleFormatType::Sub.supports_advanced_styling());
267    /// ```
268    pub fn supports_advanced_styling(&self) -> bool {
269        matches!(self, SubtitleFormatType::Ass | SubtitleFormatType::Vtt)
270    }
271
272    /// Check if the format uses frame-based timing.
273    ///
274    /// Frame-based timing requires knowledge of the video frame rate
275    /// for accurate time calculations.
276    ///
277    /// # Returns
278    ///
279    /// - `true` for SUB format
280    /// - `false` for all other formats
281    pub fn uses_frame_timing(&self) -> bool {
282        matches!(self, SubtitleFormatType::Sub)
283    }
284}
285
286impl std::fmt::Display for SubtitleFormatType {
287    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288        write!(f, "{}", self.as_str())
289    }
290}
291
292/// Unified subtitle data structure containing entries, metadata, and format information.
293///
294/// This structure represents a complete subtitle file in memory, providing a
295/// format-agnostic representation that can be converted between different
296/// subtitle formats while preserving as much information as possible.
297///
298/// # Examples
299///
300/// ```rust,ignore
301/// use subx_cli::core::formats::{Subtitle, SubtitleEntry, SubtitleMetadata, SubtitleFormatType};
302/// use std::time::Duration;
303///
304/// let subtitle = Subtitle {
305///     entries: vec![
306///         SubtitleEntry {
307///             index: 1,
308///             start_time: Duration::from_secs(10),
309///             end_time: Duration::from_secs(13),
310///             text: "Hello, world!".to_string(),
311///             styling: None,
312///         }
313///     ],
314///     metadata: SubtitleMetadata {
315///         title: Some("Movie Title".to_string()),
316///         language: Some("en".to_string()),
317///         encoding: "UTF-8".to_string(),
318///         frame_rate: Some(23.976),
319///         original_format: SubtitleFormatType::Srt,
320///     },
321///     format: SubtitleFormatType::Srt,
322/// };
323///
324/// println!("Subtitle has {} entries", subtitle.entries.len());
325/// ```
326#[derive(Debug, Clone)]
327pub struct Subtitle {
328    /// Collection of subtitle entries with timing and text content.
329    pub entries: Vec<SubtitleEntry>,
330
331    /// Metadata information about the subtitle file.
332    pub metadata: SubtitleMetadata,
333
334    /// Current format type of the subtitle data.
335    pub format: SubtitleFormatType,
336}
337
338impl Subtitle {
339    /// Create a new subtitle with the given format and metadata.
340    ///
341    /// # Arguments
342    ///
343    /// * `format` - The subtitle format type
344    /// * `metadata` - Metadata associated with the subtitle
345    ///
346    /// # Examples
347    ///
348    /// ```rust,ignore
349    /// use subx_cli::core::formats::{Subtitle, SubtitleMetadata, SubtitleFormatType};
350    ///
351    /// let metadata = SubtitleMetadata::default();
352    /// let subtitle = Subtitle::new(SubtitleFormatType::Srt, metadata);
353    /// assert_eq!(subtitle.entries.len(), 0);
354    /// ```
355    pub fn new(format: SubtitleFormatType, metadata: SubtitleMetadata) -> Self {
356        Self {
357            entries: Vec::new(),
358            metadata,
359            format,
360        }
361    }
362
363    /// Get the total duration of the subtitle file.
364    ///
365    /// Returns the time span from the first entry's start time to the
366    /// last entry's end time, or zero if there are no entries.
367    ///
368    /// # Examples
369    ///
370    /// ```rust,ignore
371    /// let duration = subtitle.total_duration();
372    /// println!("Subtitle duration: {:.2} seconds", duration.as_secs_f32());
373    /// ```
374    pub fn total_duration(&self) -> Duration {
375        if self.entries.is_empty() {
376            return Duration::ZERO;
377        }
378
379        let first_start = self.entries.first().unwrap().start_time;
380        let last_end = self.entries.last().unwrap().end_time;
381        last_end.saturating_sub(first_start)
382    }
383
384    /// Check if subtitle entries have any timing overlaps.
385    ///
386    /// Returns `true` if any entry's start time is before the previous
387    /// entry's end time, indicating overlapping subtitles.
388    pub fn has_overlaps(&self) -> bool {
389        for window in self.entries.windows(2) {
390            if window[1].start_time < window[0].end_time {
391                return true;
392            }
393        }
394        false
395    }
396
397    /// Sort entries by start time to ensure chronological order.
398    ///
399    /// This method is useful after manual manipulation of entries
400    /// or when merging subtitles from multiple sources.
401    pub fn sort_entries(&mut self) {
402        self.entries.sort_by_key(|entry| entry.start_time);
403
404        // Re-index entries after sorting
405        for (index, entry) in self.entries.iter_mut().enumerate() {
406            entry.index = index + 1;
407        }
408    }
409}
410
411/// Single subtitle entry containing timing, index, and text information.
412///
413/// This structure represents an individual subtitle entry with its timing,
414/// content, and optional styling information. It provides the basic building
415/// block for all subtitle formats.
416///
417/// # Timing Constraints
418///
419/// - `start_time` must be less than `end_time`
420/// - Times are represented as `Duration` from the beginning of the media
421/// - Minimum recommended duration is 1 second for readability
422/// - Maximum recommended duration is 7 seconds for standard subtitles
423///
424/// # Text Content
425///
426/// - Supports Unicode text for international character sets
427/// - May contain format-specific markup (HTML tags for SRT, ASS tags for ASS format)
428/// - Line breaks are preserved and format-dependent (\n, \N, or <br>)
429///
430/// # Examples
431///
432/// ```rust,ignore
433/// use subx_cli::core::formats::{SubtitleEntry, StylingInfo};
434/// use std::time::Duration;
435///
436/// // Basic subtitle entry
437/// let entry = SubtitleEntry {
438///     index: 1,
439///     start_time: Duration::from_millis(10500), // 10.5 seconds
440///     end_time: Duration::from_millis(13750),   // 13.75 seconds
441///     text: "Hello, world!".to_string(),
442///     styling: None,
443/// };
444///
445/// // Entry with styling
446/// let styled_entry = SubtitleEntry {
447///     index: 2,
448///     start_time: Duration::from_secs(15),
449///     end_time: Duration::from_secs(18),
450///     text: "<b>Bold text</b>".to_string(),
451///     styling: Some(StylingInfo {
452///         bold: true,
453///         ..Default::default()
454///     }),
455/// };
456///
457/// assert_eq!(entry.duration(), Duration::from_millis(3250));
458/// assert!(entry.is_valid_timing());
459/// ```
460#[derive(Debug, Clone)]
461pub struct SubtitleEntry {
462    /// Sequential number of the subtitle entry (1-based indexing).
463    ///
464    /// This index is used for ordering and reference purposes.
465    /// Most formats expect continuous numbering starting from 1.
466    pub index: usize,
467
468    /// Start timestamp of the subtitle entry.
469    ///
470    /// Represents when the subtitle should first appear on screen,
471    /// measured from the beginning of the media file.
472    pub start_time: Duration,
473
474    /// End timestamp of the subtitle entry.
475    ///
476    /// Represents when the subtitle should disappear from screen.
477    /// Must be greater than `start_time`.
478    pub end_time: Duration,
479
480    /// Text content of the subtitle entry.
481    ///
482    /// May contain format-specific markup for styling and line breaks.
483    /// Unicode content is fully supported for international subtitles.
484    pub text: String,
485
486    /// Optional styling information for the subtitle entry.
487    ///
488    /// Contains font, color, and formatting information. Not all formats
489    /// support styling, and some styling may be lost during conversion.
490    pub styling: Option<StylingInfo>,
491}
492
493impl SubtitleEntry {
494    /// Create a new subtitle entry with basic timing and text.
495    ///
496    /// # Arguments
497    ///
498    /// * `index` - Sequential number of the entry (1-based)
499    /// * `start_time` - When the subtitle should appear
500    /// * `end_time` - When the subtitle should disappear
501    /// * `text` - The subtitle text content
502    ///
503    /// # Panics
504    ///
505    /// Panics if `start_time >= end_time`.
506    ///
507    /// # Examples
508    ///
509    /// ```rust,ignore
510    /// use subx_cli::core::formats::SubtitleEntry;
511    /// use std::time::Duration;
512    ///
513    /// let entry = SubtitleEntry::new(
514    ///     1,
515    ///     Duration::from_secs(10),
516    ///     Duration::from_secs(13),
517    ///     "Hello!".to_string()
518    /// );
519    /// ```
520    pub fn new(index: usize, start_time: Duration, end_time: Duration, text: String) -> Self {
521        assert!(start_time < end_time, "Start time must be before end time");
522
523        Self {
524            index,
525            start_time,
526            end_time,
527            text,
528            styling: None,
529        }
530    }
531
532    /// Calculate the duration of this subtitle entry.
533    ///
534    /// Returns the time span from start to end of the subtitle.
535    ///
536    /// # Examples
537    ///
538    /// ```rust,ignore
539    /// assert_eq!(entry.duration(), Duration::from_secs(3));
540    /// ```
541    pub fn duration(&self) -> Duration {
542        self.end_time.saturating_sub(self.start_time)
543    }
544
545    /// Check if the timing of this entry is valid.
546    ///
547    /// Returns `true` if start_time < end_time and both are valid durations.
548    pub fn is_valid_timing(&self) -> bool {
549        self.start_time < self.end_time
550    }
551
552    /// Check if this entry overlaps with another entry.
553    ///
554    /// # Arguments
555    ///
556    /// * `other` - Another subtitle entry to check against
557    ///
558    /// # Returns
559    ///
560    /// Returns `true` if the time ranges overlap.
561    pub fn overlaps_with(&self, other: &SubtitleEntry) -> bool {
562        self.start_time < other.end_time && other.start_time < self.end_time
563    }
564
565    /// Get the text content without any format-specific markup.
566    ///
567    /// Removes common formatting tags like HTML tags for SRT format.
568    /// This is useful for text analysis and search operations.
569    pub fn plain_text(&self) -> String {
570        // Basic HTML tag removal for SRT format
571        let mut text = self.text.clone();
572
573        // Remove common HTML tags
574        let tags = [
575            "<b>",
576            "</b>",
577            "<i>",
578            "</i>",
579            "<u>",
580            "</u>",
581            "<font[^>]*>",
582            "</font>",
583            "<br>",
584            "<br/>",
585        ];
586
587        for tag in &tags {
588            if tag.contains('[') {
589                // Use regex for complex patterns (simplified for example)
590                text = text.replace(tag, "");
591            } else {
592                text = text.replace(tag, " ");
593            }
594        }
595
596        // Clean up extra whitespace
597        text.split_whitespace().collect::<Vec<_>>().join(" ")
598    }
599}
600
601/// Metadata associated with a subtitle file, containing format and content information.
602///
603/// This structure holds descriptive information about the subtitle file that
604/// may be embedded in the file format or derived during processing. It helps
605/// maintain context during format conversions and provides useful information
606/// for subtitle management.
607///
608/// # Fields Description
609///
610/// - `title`: Optional title of the media or subtitle content
611/// - `language`: Language code (ISO 639-1/639-3) for the subtitle content
612/// - `encoding`: Character encoding used in the original file
613/// - `frame_rate`: Video frame rate for frame-based timing formats
614/// - `original_format`: The source format before any conversions
615///
616/// # Examples
617///
618/// ```rust,ignore
619/// use subx_cli::core::formats::{SubtitleMetadata, SubtitleFormatType};
620///
621/// let metadata = SubtitleMetadata {
622///     title: Some("Episode 1".to_string()),
623///     language: Some("en".to_string()),
624///     encoding: "UTF-8".to_string(),
625///     frame_rate: Some(23.976),
626///     original_format: SubtitleFormatType::Srt,
627/// };
628///
629/// assert!(metadata.is_frame_based());
630/// assert_eq!(metadata.display_name(), "Episode 1 (English)");
631/// ```
632#[derive(Debug, Clone)]
633pub struct SubtitleMetadata {
634    /// Optional title of the subtitle content or associated media.
635    ///
636    /// This may be extracted from the subtitle file header or derived
637    /// from the filename. Used for display and organization purposes.
638    pub title: Option<String>,
639
640    /// Language code for the subtitle content.
641    ///
642    /// Uses ISO 639-1 (2-letter) or ISO 639-3 (3-letter) codes.
643    /// Examples: "en", "zh", "ja", "chi", "eng"
644    pub language: Option<String>,
645
646    /// Character encoding of the original subtitle file.
647    ///
648    /// Common values: "UTF-8", "UTF-16", "GB2312", "BIG5", "Shift_JIS"
649    /// This information is crucial for proper text decoding.
650    pub encoding: String,
651
652    /// Video frame rate for frame-based timing calculations.
653    ///
654    /// Required for SUB format and useful for timing validation.
655    /// Common values: 23.976, 24.0, 25.0, 29.97, 30.0
656    pub frame_rate: Option<f32>,
657
658    /// Original format type before any conversions.
659    ///
660    /// Tracks the source format to maintain conversion history
661    /// and format-specific feature compatibility.
662    pub original_format: SubtitleFormatType,
663}
664
665impl SubtitleMetadata {
666    /// Create new metadata with default values and specified format.
667    ///
668    /// # Arguments
669    ///
670    /// * `format` - The original format type
671    ///
672    /// # Examples
673    ///
674    /// ```rust,ignore
675    /// let metadata = SubtitleMetadata::new(SubtitleFormatType::Srt);
676    /// assert_eq!(metadata.encoding, "UTF-8");
677    /// ```
678    pub fn new(format: SubtitleFormatType) -> Self {
679        Self {
680            title: None,
681            language: None,
682            encoding: "UTF-8".to_string(),
683            frame_rate: None,
684            original_format: format,
685        }
686    }
687
688    /// Check if the subtitle uses frame-based timing.
689    ///
690    /// Returns `true` if the format requires frame rate information.
691    pub fn is_frame_based(&self) -> bool {
692        self.original_format.uses_frame_timing()
693    }
694
695    /// Generate a display-friendly name for the subtitle.
696    ///
697    /// Combines title and language information for user presentation.
698    ///
699    /// # Returns
700    ///
701    /// A formatted string like "Title (Language)" or just "Language" if no title.
702    pub fn display_name(&self) -> String {
703        match (&self.title, &self.language) {
704            (Some(title), Some(lang)) => format!("{} ({})", title, lang.to_uppercase()),
705            (Some(title), None) => title.clone(),
706            (None, Some(lang)) => lang.to_uppercase(),
707            (None, None) => "Unknown".to_string(),
708        }
709    }
710
711    /// Check if the metadata contains complete information.
712    ///
713    /// Returns `true` if title, language, and frame rate (when needed) are set.
714    pub fn is_complete(&self) -> bool {
715        self.title.is_some()
716            && self.language.is_some()
717            && (!self.is_frame_based() || self.frame_rate.is_some())
718    }
719}
720
721impl Default for SubtitleMetadata {
722    fn default() -> Self {
723        Self::new(SubtitleFormatType::Srt)
724    }
725}
726
727/// Optional styling information for subtitle entries with comprehensive formatting support.
728///
729/// This structure contains visual formatting information that can be applied to
730/// subtitle text. Not all formats support all styling options, and some styling
731/// may be lost during format conversions.
732///
733/// # Format Support
734///
735/// - **SRT**: Limited support (bold, italic, underline via HTML tags)
736/// - **ASS**: Full support for all styling options plus advanced features
737/// - **VTT**: Good support via CSS-style declarations
738/// - **SUB**: No styling support (ignored)
739///
740/// # Color Format
741///
742/// Colors can be specified in various formats:
743/// - Hex: "#FF0000", "#ff0000"
744/// - RGB: "rgb(255, 0, 0)"
745/// - Named: "red", "blue", "white"
746/// - ASS format: "&H0000FF&" (BGR order)
747///
748/// # Examples
749///
750/// ```rust,ignore
751/// use subx_cli::core::formats::StylingInfo;
752///
753/// // Basic text styling
754/// let basic_style = StylingInfo {
755///     bold: true,
756///     italic: false,
757///     underline: false,
758///     ..Default::default()
759/// };
760///
761/// // Complete styling with font and color
762/// let full_style = StylingInfo {
763///     font_name: Some("Arial".to_string()),
764///     font_size: Some(20),
765///     color: Some("#FFFFFF".to_string()),
766///     bold: true,
767///     italic: false,
768///     underline: false,
769/// };
770///
771/// assert!(full_style.has_font_styling());
772/// assert!(full_style.has_text_decoration());
773/// ```
774#[derive(Debug, Clone, Default)]
775pub struct StylingInfo {
776    /// Font family name for the subtitle text.
777    ///
778    /// Common fonts: "Arial", "Times New Roman", "Helvetica", "SimHei"
779    /// Some formats may fall back to default fonts if not available.
780    pub font_name: Option<String>,
781
782    /// Font size in points or pixels (format-dependent).
783    ///
784    /// Typical ranges: 12-24 for normal subtitles, larger for accessibility.
785    /// The exact interpretation depends on the target format.
786    pub font_size: Option<u32>,
787
788    /// Text color specification.
789    ///
790    /// Supports multiple formats: hex (#FF0000), RGB, named colors.
791    /// Default is usually white (#FFFFFF) for video subtitles.
792    pub color: Option<String>,
793
794    /// Whether the text should be rendered in bold weight.
795    pub bold: bool,
796
797    /// Whether the text should be rendered in italic style.
798    pub italic: bool,
799
800    /// Whether the text should have underline decoration.
801    pub underline: bool,
802}
803
804impl StylingInfo {
805    /// Create new styling with only text decorations.
806    ///
807    /// # Arguments
808    ///
809    /// * `bold` - Apply bold weight
810    /// * `italic` - Apply italic style
811    /// * `underline` - Apply underline decoration
812    ///
813    /// # Examples
814    ///
815    /// ```rust,ignore
816    /// let style = StylingInfo::new(true, false, false); // Bold only
817    /// ```
818    pub fn new(bold: bool, italic: bool, underline: bool) -> Self {
819        Self {
820            font_name: None,
821            font_size: None,
822            color: None,
823            bold,
824            italic,
825            underline,
826        }
827    }
828
829    /// Check if any font-related styling is applied.
830    ///
831    /// Returns `true` if font name, size, or color is specified.
832    pub fn has_font_styling(&self) -> bool {
833        self.font_name.is_some() || self.font_size.is_some() || self.color.is_some()
834    }
835
836    /// Check if any text decoration is applied.
837    ///
838    /// Returns `true` if bold, italic, or underline is enabled.
839    pub fn has_text_decoration(&self) -> bool {
840        self.bold || self.italic || self.underline
841    }
842
843    /// Check if any styling is applied at all.
844    ///
845    /// Returns `true` if either font styling or text decoration is present.
846    pub fn has_any_styling(&self) -> bool {
847        self.has_font_styling() || self.has_text_decoration()
848    }
849
850    /// Convert color to hex format if possible.
851    ///
852    /// Attempts to normalize the color specification to #RRGGBB format.
853    /// Returns the original color string if conversion is not possible.
854    pub fn normalized_color(&self) -> Option<String> {
855        self.color.as_ref().map(|color| {
856            if color.starts_with('#') && color.len() == 7 {
857                color.to_uppercase()
858            } else if color.starts_with("rgb(") {
859                // Basic RGB parsing (simplified)
860                color.clone() // Would need proper RGB parsing
861            } else {
862                // Named colors - would need color name mapping
863                color.clone()
864            }
865        })
866    }
867
868    /// Generate CSS-style representation of the styling.
869    ///
870    /// Creates a CSS declaration block that can be used for VTT format
871    /// or web-based subtitle rendering.
872    ///
873    /// # Returns
874    ///
875    /// CSS string like "font-family: Arial; font-weight: bold; color: #FF0000;"
876    pub fn to_css(&self) -> String {
877        let mut css = Vec::new();
878
879        if let Some(font) = &self.font_name {
880            css.push(format!("font-family: {}", font));
881        }
882
883        if let Some(size) = self.font_size {
884            css.push(format!("font-size: {}pt", size));
885        }
886
887        if let Some(color) = &self.color {
888            css.push(format!("color: {}", color));
889        }
890
891        if self.bold {
892            css.push("font-weight: bold".to_string());
893        }
894
895        if self.italic {
896            css.push("font-style: italic".to_string());
897        }
898
899        if self.underline {
900            css.push("text-decoration: underline".to_string());
901        }
902
903        css.join("; ")
904    }
905}
906
907/// Trait defining the interface for subtitle format parsing, serialization, and detection.
908///
909/// This trait provides a unified interface for working with different subtitle formats.
910/// Each format implementation provides specific parsing and serialization logic while
911/// maintaining a consistent API for format detection and conversion.
912///
913/// # Implementation Requirements
914///
915/// Implementors must provide:
916/// - **Parsing**: Convert raw text content to structured `Subtitle` data
917/// - **Serialization**: Convert structured data back to format-specific text
918/// - **Detection**: Identify if content belongs to this format
919/// - **Metadata**: Format name and supported file extensions
920///
921/// # Format Detection Priority
922///
923/// When multiple formats claim to support content, detection should be:
924/// 1. **Strict**: Prefer specific format markers over generic patterns
925/// 2. **Fast**: Use lightweight checks before expensive parsing
926/// 3. **Reliable**: Minimize false positives for robust format identification
927///
928/// # Error Handling
929///
930/// All parsing and serialization methods should return `crate::Result<T>` to
931/// provide detailed error information about format-specific failures.
932///
933/// # Examples
934///
935/// ```rust,ignore
936/// use subx_cli::core::formats::{SubtitleFormat, Subtitle};
937///
938/// struct MyFormat;
939///
940/// impl SubtitleFormat for MyFormat {
941///     fn parse(&self, content: &str) -> crate::Result<Subtitle> {
942///         // Format-specific parsing logic
943///         todo!()
944///     }
945///     
946///     fn serialize(&self, subtitle: &Subtitle) -> crate::Result<String> {
947///         // Format-specific serialization logic
948///         todo!()
949///     }
950///     
951///     fn detect(&self, content: &str) -> bool {
952///         // Check for format-specific markers
953///         content.contains("my_format_marker")
954///     }
955///     
956///     fn format_name(&self) -> &'static str {
957///         "My Format"
958///     }
959///     
960///     fn file_extensions(&self) -> &'static [&'static str] {
961///         &["myf"]
962///     }
963/// }
964///
965/// // Usage
966/// let format = MyFormat;
967/// let content = "...subtitle content...";
968///
969/// if format.detect(content) {
970///     let subtitle = format.parse(content)?;
971///     println!("Parsed {} entries", subtitle.entries.len());
972/// }
973/// ```
974pub trait SubtitleFormat {
975    /// Parse subtitle content into a structured `Subtitle` data structure.
976    ///
977    /// This method converts raw subtitle file content into the unified
978    /// `Subtitle` representation, handling format-specific timing,
979    /// text content, and styling information.
980    ///
981    /// # Arguments
982    ///
983    /// * `content` - Raw subtitle file content as UTF-8 string
984    ///
985    /// # Returns
986    ///
987    /// Returns a `Subtitle` struct containing:
988    /// - Parsed subtitle entries with timing and text
989    /// - Metadata extracted from the file content
990    /// - Format type information
991    ///
992    /// # Errors
993    ///
994    /// Returns an error if:
995    /// - Content is not valid for this format
996    /// - Timing information is malformed or invalid
997    /// - Text encoding issues are encountered
998    /// - Required format elements are missing
999    ///
1000    /// # Implementation Notes
1001    ///
1002    /// - Should be tolerant of minor formatting variations
1003    /// - Must validate timing consistency (start < end)
1004    /// - Should preserve as much styling information as possible
1005    /// - May apply format-specific text normalization
1006    ///
1007    /// # Examples
1008    ///
1009    /// ```rust,ignore
1010    /// let srt_content = "1\n00:00:10,500 --> 00:00:13,000\nHello World!\n\n";
1011    /// let subtitle = format.parse(srt_content)?;
1012    /// assert_eq!(subtitle.entries.len(), 1);
1013    /// assert_eq!(subtitle.entries[0].text, "Hello World!");
1014    /// ```
1015    fn parse(&self, content: &str) -> crate::Result<Subtitle>;
1016
1017    /// Serialize a `Subtitle` structure into format-specific text representation.
1018    ///
1019    /// This method converts the unified subtitle data structure back into
1020    /// the raw text format, applying format-specific timing, styling,
1021    /// and text formatting rules.
1022    ///
1023    /// # Arguments
1024    ///
1025    /// * `subtitle` - Structured subtitle data to serialize
1026    ///
1027    /// # Returns
1028    ///
1029    /// Returns a formatted string that can be written to a subtitle file.
1030    /// The output should be valid for the target format and compatible
1031    /// with standard media players.
1032    ///
1033    /// # Errors
1034    ///
1035    /// Returns an error if:
1036    /// - Subtitle data contains invalid timing information
1037    /// - Styling information cannot be represented in the target format
1038    /// - Text content contains unsupported characters or formatting
1039    /// - Required metadata is missing for the format
1040    ///
1041    /// # Implementation Notes
1042    ///
1043    /// - Should generate clean, standards-compliant output
1044    /// - Must handle timing precision appropriate for the format
1045    /// - Should gracefully degrade unsupported styling features
1046    /// - May need to validate or adjust entry ordering
1047    ///
1048    /// # Examples
1049    ///
1050    /// ```rust,ignore
1051    /// let output = format.serialize(&subtitle)?;
1052    /// std::fs::write("output.srt", output)?;
1053    /// ```
1054    fn serialize(&self, subtitle: &Subtitle) -> crate::Result<String>;
1055
1056    /// Detect whether the provided content matches this subtitle format.
1057    ///
1058    /// This method performs lightweight content analysis to determine if
1059    /// the raw text content belongs to this subtitle format. It should
1060    /// be fast and reliable for format identification.
1061    ///
1062    /// # Arguments
1063    ///
1064    /// * `content` - Raw subtitle file content to analyze
1065    ///
1066    /// # Returns
1067    ///
1068    /// Returns `true` if the content appears to be in this format.
1069    /// Should minimize false positives while catching valid content.
1070    ///
1071    /// # Implementation Guidelines
1072    ///
1073    /// - Look for format-specific markers or patterns
1074    /// - Check timing format conventions
1075    /// - Validate structural elements (headers, separators)
1076    /// - Avoid expensive parsing in detection
1077    /// - Be conservative to prevent false matches
1078    ///
1079    /// # Examples
1080    ///
1081    /// ```rust,ignore
1082    /// let srt_content = "1\n00:00:10,500 --> 00:00:13,000\nText\n\n";
1083    /// assert!(srt_format.detect(srt_content));
1084    ///
1085    /// let ass_content = "[Script Info]\nTitle: Test\n[V4+ Styles]\n";
1086    /// assert!(ass_format.detect(ass_content));
1087    /// ```
1088    fn detect(&self, content: &str) -> bool;
1089
1090    /// Returns the human-readable name of this subtitle format.
1091    ///
1092    /// This name is used for user interfaces, error messages, and
1093    /// format selection dialogs. It should be clear and descriptive.
1094    ///
1095    /// # Examples
1096    ///
1097    /// ```rust,ignore
1098    /// assert_eq!(srt_format.format_name(), "SubRip Text");
1099    /// assert_eq!(ass_format.format_name(), "Advanced SubStation Alpha");
1100    /// assert_eq!(vtt_format.format_name(), "WebVTT");
1101    /// ```
1102    fn format_name(&self) -> &'static str;
1103
1104    /// Returns the list of supported file extensions for this format.
1105    ///
1106    /// Extensions should be lowercase without the leading dot.
1107    /// The primary extension should be listed first.
1108    ///
1109    /// # Returns
1110    ///
1111    /// Array of extension strings, with primary extension first.
1112    ///
1113    /// # Examples
1114    ///
1115    /// ```rust,ignore
1116    /// assert_eq!(srt_format.file_extensions(), &["srt"]);
1117    /// assert_eq!(ass_format.file_extensions(), &["ass", "ssa"]);
1118    /// assert_eq!(vtt_format.file_extensions(), &["vtt"]);
1119    /// ```
1120    fn file_extensions(&self) -> &'static [&'static str];
1121
1122    /// Check if this format supports advanced styling features.
1123    ///
1124    /// Returns `true` if the format can handle fonts, colors, positioning,
1125    /// and other advanced subtitle styling.
1126    ///
1127    /// # Default Implementation
1128    ///
1129    /// The default implementation returns `false`. Formats with styling
1130    /// support should override this method.
1131    fn supports_styling(&self) -> bool {
1132        false
1133    }
1134
1135    /// Check if this format uses frame-based timing.
1136    ///
1137    /// Returns `true` if timing is based on frame numbers rather than
1138    /// absolute time, requiring frame rate information for conversion.
1139    ///
1140    /// # Default Implementation
1141    ///
1142    /// The default implementation returns `false`. Frame-based formats
1143    /// should override this method.
1144    fn uses_frame_timing(&self) -> bool {
1145        false
1146    }
1147}