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}