Skip to main content

ass_core/parser/ast/script_info/
info.rs

1//! `ScriptInfo` struct definition and metadata accessors
2//!
3//! Defines the zero-copy [`ScriptInfo`] AST node for the [Script Info]
4//! section along with its field accessors and ASS serialization helper.
5
6use alloc::vec::Vec;
7
8#[cfg(not(feature = "std"))]
9extern crate alloc;
10
11use super::Span;
12#[cfg(debug_assertions)]
13use core::ops::Range;
14
15/// Script Info section containing metadata and headers
16///
17/// Represents the [Script Info] section of an ASS file as key-value pairs
18/// with zero-copy string references. Provides convenient accessor methods
19/// for standard ASS metadata fields.
20///
21/// # Examples
22///
23/// ```rust
24/// use ass_core::parser::ast::{ScriptInfo, Span};
25///
26/// let fields = vec![("Title", "Test Script"), ("ScriptType", "v4.00+")];
27/// let info = ScriptInfo { fields, span: Span::new(0, 0, 0, 0) };
28///
29/// assert_eq!(info.title(), "Test Script");
30/// assert_eq!(info.script_type(), Some("v4.00+"));
31/// ```
32#[derive(Debug, Clone, PartialEq, Eq)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize))]
34pub struct ScriptInfo<'a> {
35    /// Key-value pairs as zero-copy spans
36    pub fields: Vec<(&'a str, &'a str)>,
37    /// Span in source text where this script info section is defined
38    pub span: Span,
39}
40
41impl<'a> ScriptInfo<'a> {
42    /// Get field value by key (case-sensitive)
43    ///
44    /// Searches for the specified key in the script info fields and
45    /// returns the associated value if found.
46    ///
47    /// # Arguments
48    ///
49    /// * `key` - Field name to search for
50    ///
51    /// # Returns
52    ///
53    /// The field value if found, `None` otherwise
54    ///
55    /// # Examples
56    ///
57    /// ```rust
58    /// # use ass_core::parser::ast::{ScriptInfo, Span};
59    /// let fields = vec![("Title", "Test"), ("Author", "User")];
60    /// let info = ScriptInfo { fields, span: Span::new(0, 0, 0, 0) };
61    ///
62    /// assert_eq!(info.get_field("Title"), Some("Test"));
63    /// assert_eq!(info.get_field("Unknown"), None);
64    /// ```
65    #[must_use]
66    pub fn get_field(&self, key: &str) -> Option<&'a str> {
67        self.fields.iter().find(|(k, _)| *k == key).map(|(_, v)| *v)
68    }
69
70    /// Get script title, defaulting to `<untitled>`
71    ///
72    /// Returns the "Title" field value or a default if not specified.
73    /// This is a convenience method for the most commonly accessed field.
74    #[must_use]
75    pub fn title(&self) -> &str {
76        self.get_field("Title").unwrap_or("<untitled>")
77    }
78
79    /// Get script type version
80    ///
81    /// Returns the "`ScriptType`" field which indicates the ASS version
82    /// and feature compatibility (e.g., "v4.00+", "v4.00").
83    #[must_use]
84    pub fn script_type(&self) -> Option<&'a str> {
85        self.get_field("ScriptType")
86    }
87
88    /// Get play resolution as (width, height)
89    ///
90    /// Parses `PlayResX` and `PlayResY` fields to determine the intended
91    /// video resolution for subtitle rendering.
92    ///
93    /// # Returns
94    ///
95    /// Tuple of (width, height) if both fields are present and valid,
96    /// `None` if either field is missing or invalid.
97    #[must_use]
98    pub fn play_resolution(&self) -> Option<(u32, u32)> {
99        let width = self.get_field("PlayResX")?.parse().ok()?;
100        let height = self.get_field("PlayResY")?.parse().ok()?;
101        Some((width, height))
102    }
103
104    /// Get layout resolution as (width, height)
105    ///
106    /// Layout resolution defines the coordinate system for positioning and scaling
107    /// subtitles relative to the video resolution. Used by style analysis for
108    /// proper layout calculations.
109    ///
110    /// # Returns
111    ///
112    /// Tuple of (width, height) if both fields are present and valid,
113    /// `None` if either field is missing or invalid.
114    #[must_use]
115    pub fn layout_resolution(&self) -> Option<(u32, u32)> {
116        let width = self.get_field("LayoutResX")?.parse().ok()?;
117        let height = self.get_field("LayoutResY")?.parse().ok()?;
118        Some((width, height))
119    }
120
121    /// Get wrap style setting
122    ///
123    /// Returns the `WrapStyle` field which controls how long lines are wrapped.
124    /// Defaults to 0 (smart wrapping) if not specified.
125    ///
126    /// # Wrap Styles
127    ///
128    /// - 0: Smart wrapping (default)
129    /// - 1: End-of-line wrapping
130    /// - 2: No wrapping
131    /// - 3: Smart wrapping with lower line longer
132    #[must_use]
133    pub fn wrap_style(&self) -> u8 {
134        self.get_field("WrapStyle")
135            .and_then(|s| s.parse().ok())
136            .unwrap_or(0)
137    }
138
139    /// Convert script info to ASS string representation
140    ///
141    /// Generates the [Script Info] section with all fields.
142    ///
143    /// # Examples
144    ///
145    /// ```rust
146    /// # use ass_core::parser::ast::{ScriptInfo, Span};
147    /// let fields = vec![("Title", "Test Script"), ("ScriptType", "v4.00+")];
148    /// let info = ScriptInfo { fields, span: Span::new(0, 0, 0, 0) };
149    /// let ass_string = info.to_ass_string();
150    /// assert!(ass_string.contains("[Script Info]"));
151    /// assert!(ass_string.contains("Title: Test Script"));
152    /// assert!(ass_string.contains("ScriptType: v4.00+"));
153    /// ```
154    #[must_use]
155    pub fn to_ass_string(&self) -> alloc::string::String {
156        use core::fmt::Write;
157        let mut result = alloc::string::String::from("[Script Info]\n");
158        for (key, value) in &self.fields {
159            let _ = writeln!(result, "{key}: {value}");
160        }
161        result
162    }
163
164    /// Validate all spans in this `ScriptInfo` reference valid source
165    ///
166    /// Debug helper to ensure zero-copy invariants are maintained.
167    /// Validates that all string references point to memory within
168    /// the specified source range.
169    ///
170    /// Only available in debug builds to avoid performance overhead.
171    #[cfg(debug_assertions)]
172    #[must_use]
173    pub fn validate_spans(&self, source_range: &Range<usize>) -> bool {
174        self.fields.iter().all(|(key, value)| {
175            let key_ptr = key.as_ptr() as usize;
176            let value_ptr = value.as_ptr() as usize;
177            source_range.contains(&key_ptr) && source_range.contains(&value_ptr)
178        })
179    }
180}