ass_core/parser/ast/
script_info.rs

1//! Script Info AST node for ASS script metadata
2//!
3//! Contains the `ScriptInfo` struct representing the [Script Info] section
4//! of ASS files with zero-copy design and convenient accessor methods
5//! for common metadata fields.
6
7use alloc::vec::Vec;
8
9#[cfg(not(feature = "std"))]
10extern crate alloc;
11
12use super::Span;
13#[cfg(debug_assertions)]
14use core::ops::Range;
15
16/// Script Info section containing metadata and headers
17///
18/// Represents the [Script Info] section of an ASS file as key-value pairs
19/// with zero-copy string references. Provides convenient accessor methods
20/// for standard ASS metadata fields.
21///
22/// # Examples
23///
24/// ```rust
25/// use ass_core::parser::ast::{ScriptInfo, Span};
26///
27/// let fields = vec![("Title", "Test Script"), ("ScriptType", "v4.00+")];
28/// let info = ScriptInfo { fields, span: Span::new(0, 0, 0, 0) };
29///
30/// assert_eq!(info.title(), "Test Script");
31/// assert_eq!(info.script_type(), Some("v4.00+"));
32/// ```
33#[derive(Debug, Clone, PartialEq, Eq)]
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}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    #[cfg(not(feature = "std"))]
186    use alloc::vec;
187
188    #[test]
189    fn script_info_field_access() {
190        let fields = vec![("Title", "Test Script"), ("ScriptType", "v4.00+")];
191        let info = ScriptInfo {
192            fields,
193            span: Span::new(0, 0, 0, 0),
194        };
195
196        assert_eq!(info.title(), "Test Script");
197        assert_eq!(info.script_type(), Some("v4.00+"));
198        assert_eq!(info.get_field("Unknown"), None);
199    }
200
201    #[test]
202    fn script_info_defaults() {
203        let info = ScriptInfo {
204            fields: Vec::new(),
205            span: Span::new(0, 0, 0, 0),
206        };
207        assert_eq!(info.title(), "<untitled>");
208        assert_eq!(info.wrap_style(), 0);
209        assert_eq!(info.layout_resolution(), None);
210        assert_eq!(info.play_resolution(), None);
211    }
212
213    #[test]
214    fn script_info_play_resolution() {
215        let fields = vec![("PlayResX", "1920"), ("PlayResY", "1080")];
216        let info = ScriptInfo {
217            fields,
218            span: Span::new(0, 0, 0, 0),
219        };
220        assert_eq!(info.play_resolution(), Some((1920, 1080)));
221    }
222
223    #[test]
224    fn script_info_partial_play_resolution() {
225        let fields = vec![("PlayResX", "1920")];
226        let info = ScriptInfo {
227            fields,
228            span: Span::new(0, 0, 0, 0),
229        };
230        assert_eq!(info.play_resolution(), None);
231    }
232
233    #[test]
234    fn script_info_layout_resolution() {
235        let fields = vec![("LayoutResX", "1920"), ("LayoutResY", "1080")];
236        let info = ScriptInfo {
237            fields,
238            span: Span::new(0, 0, 0, 0),
239        };
240        assert_eq!(info.layout_resolution(), Some((1920, 1080)));
241    }
242
243    #[test]
244    fn script_info_partial_layout_resolution() {
245        let fields = vec![("LayoutResX", "1920")];
246        let info = ScriptInfo {
247            fields,
248            span: Span::new(0, 0, 0, 0),
249        };
250        assert_eq!(info.layout_resolution(), None);
251    }
252
253    #[test]
254    fn script_info_wrap_style() {
255        let fields = vec![("WrapStyle", "2")];
256        let info = ScriptInfo {
257            fields,
258            span: Span::new(0, 0, 0, 0),
259        };
260        assert_eq!(info.wrap_style(), 2);
261    }
262
263    #[test]
264    fn script_info_invalid_wrap_style() {
265        let fields = vec![("WrapStyle", "invalid")];
266        let info = ScriptInfo {
267            fields,
268            span: Span::new(0, 0, 0, 0),
269        };
270        assert_eq!(info.wrap_style(), 0); // Default fallback
271    }
272
273    #[test]
274    fn script_info_invalid_resolution() {
275        let fields = vec![("PlayResX", "invalid"), ("PlayResY", "1080")];
276        let info = ScriptInfo {
277            fields,
278            span: Span::new(0, 0, 0, 0),
279        };
280        assert_eq!(info.play_resolution(), None);
281    }
282
283    #[test]
284    fn script_info_case_sensitive_keys() {
285        let fields = vec![("title", "Test"), ("Title", "Correct")];
286        let info = ScriptInfo {
287            fields,
288            span: Span::new(0, 0, 0, 0),
289        };
290        assert_eq!(info.get_field("Title"), Some("Correct"));
291        assert_eq!(info.get_field("title"), Some("Test"));
292    }
293
294    #[test]
295    fn script_info_to_ass_string() {
296        let fields = vec![
297            ("Title", "Test Script"),
298            ("ScriptType", "v4.00+"),
299            ("WrapStyle", "0"),
300            ("ScaledBorderAndShadow", "yes"),
301            ("YCbCr Matrix", "None"),
302        ];
303        let info = ScriptInfo {
304            fields,
305            span: Span::new(0, 0, 0, 0),
306        };
307
308        let ass_string = info.to_ass_string();
309        assert!(ass_string.starts_with("[Script Info]\n"));
310        assert!(ass_string.contains("Title: Test Script\n"));
311        assert!(ass_string.contains("ScriptType: v4.00+\n"));
312        assert!(ass_string.contains("WrapStyle: 0\n"));
313        assert!(ass_string.contains("ScaledBorderAndShadow: yes\n"));
314        assert!(ass_string.contains("YCbCr Matrix: None\n"));
315    }
316
317    #[test]
318    fn script_info_to_ass_string_empty() {
319        let info = ScriptInfo {
320            fields: vec![],
321            span: Span::new(0, 0, 0, 0),
322        };
323
324        let ass_string = info.to_ass_string();
325        assert_eq!(ass_string, "[Script Info]\n");
326    }
327}