Skip to main content

ass_core/parser/sections/styles/
parse_line.rs

1//! Standalone single-line style parsing for incremental updates.
2//!
3//! Provides [`StylesParser::parse_style_line`], a stateless entry point used by
4//! incremental parsing to materialize a single [`Style`] from a line and format.
5
6use super::StylesParser;
7use crate::parser::{
8    ast::{Span, Style},
9    errors::ParseError,
10};
11use alloc::vec::Vec;
12
13impl<'a> StylesParser<'a> {
14    /// Parse a single style line
15    ///
16    /// Parses a single style definition line using the provided format specification.
17    /// This method is exposed for incremental parsing support.
18    ///
19    /// # Arguments
20    ///
21    /// * `line` - The style line to parse (without "Style:" prefix)
22    /// * `format` - The format fields from the Format line
23    /// * `line_number` - The line number for error reporting
24    ///
25    /// # Returns
26    ///
27    /// Parsed Style or error if the line is malformed
28    ///
29    /// # Errors
30    ///
31    /// Returns [`ParseError::InsufficientFields`] if the line has fewer fields than expected by format
32    pub fn parse_style_line(
33        line: &'a str,
34        format: &[&'a str],
35        line_number: u32,
36    ) -> core::result::Result<Style<'a>, ParseError> {
37        // First check if this is an inheritance style
38        let (adjusted_line, parent_style) = if line.trim_start().starts_with('*') {
39            // Find the first comma after the asterisk to extract parent style
40            line.find(',').map_or((line, None), |first_comma| {
41                let parent_part = &line[0..first_comma];
42                let parent_name = parent_part.trim_start().trim_start_matches('*').trim();
43                let remaining = &line[first_comma + 1..];
44                (remaining, Some(parent_name))
45            })
46        } else {
47            (line, None)
48        };
49
50        let parts: Vec<&str> = adjusted_line.split(',').collect();
51
52        let format = if format.is_empty() {
53            &[
54                "Name",
55                "Fontname",
56                "Fontsize",
57                "PrimaryColour",
58                "SecondaryColour",
59                "OutlineColour",
60                "BackColour",
61                "Bold",
62                "Italic",
63                "Underline",
64                "StrikeOut",
65                "ScaleX",
66                "ScaleY",
67                "Spacing",
68                "Angle",
69                "BorderStyle",
70                "Outline",
71                "Shadow",
72                "Alignment",
73                "MarginL",
74                "MarginR",
75                "MarginV",
76                "Encoding",
77            ]
78        } else {
79            format
80        };
81
82        if parts.len() < format.len() {
83            return Err(ParseError::InsufficientFields {
84                expected: format.len(),
85                found: parts.len(),
86                line: line_number as usize,
87            });
88        }
89
90        let get_field = |name: &str| -> &'a str {
91            format
92                .iter()
93                .position(|&field| field.eq_ignore_ascii_case(name))
94                .and_then(|idx| parts.get(idx))
95                .map_or("", |s| s.trim())
96        };
97
98        // Create span for the style (caller will need to adjust this)
99        let span = Span::new(0, 0, line_number, 1);
100
101        Ok(Style {
102            name: get_field("Name"),
103            parent: parent_style,
104            fontname: get_field("Fontname"),
105            fontsize: get_field("Fontsize"),
106            primary_colour: get_field("PrimaryColour"),
107            secondary_colour: get_field("SecondaryColour"),
108            outline_colour: get_field("OutlineColour"),
109            back_colour: get_field("BackColour"),
110            bold: get_field("Bold"),
111            italic: get_field("Italic"),
112            underline: get_field("Underline"),
113            strikeout: get_field("StrikeOut"),
114            scale_x: get_field("ScaleX"),
115            scale_y: get_field("ScaleY"),
116            spacing: get_field("Spacing"),
117            angle: get_field("Angle"),
118            border_style: get_field("BorderStyle"),
119            outline: get_field("Outline"),
120            shadow: get_field("Shadow"),
121            alignment: get_field("Alignment"),
122            margin_l: get_field("MarginL"),
123            margin_r: get_field("MarginR"),
124            margin_v: get_field("MarginV"),
125            margin_t: format
126                .iter()
127                .any(|&f| f.eq_ignore_ascii_case("MarginT"))
128                .then(|| get_field("MarginT")),
129            margin_b: format
130                .iter()
131                .any(|&f| f.eq_ignore_ascii_case("MarginB"))
132                .then(|| get_field("MarginB")),
133            encoding: get_field("Encoding"),
134            relative_to: format
135                .iter()
136                .any(|&f| f.eq_ignore_ascii_case("RelativeTo"))
137                .then(|| get_field("RelativeTo")),
138            span,
139        })
140    }
141}