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}