1use crate::parser::{
7 ast::{Section, Span, Style},
8 errors::{IssueCategory, IssueSeverity, ParseError, ParseIssue},
9 position_tracker::PositionTracker,
10 sections::SectionParseResult,
11 ParseResult,
12};
13use alloc::{format, vec, vec::Vec};
14
15pub struct StylesParser<'a> {
26 tracker: PositionTracker<'a>,
28 issues: Vec<ParseIssue>,
30 format: Option<Vec<&'a str>>,
32}
33
34impl<'a> StylesParser<'a> {
35 pub fn parse_style_line(
54 line: &'a str,
55 format: &[&'a str],
56 line_number: u32,
57 ) -> core::result::Result<Style<'a>, ParseError> {
58 let (adjusted_line, parent_style) = if line.trim_start().starts_with('*') {
60 line.find(',').map_or((line, None), |first_comma| {
62 let parent_part = &line[0..first_comma];
63 let parent_name = parent_part.trim_start().trim_start_matches('*').trim();
64 let remaining = &line[first_comma + 1..];
65 (remaining, Some(parent_name))
66 })
67 } else {
68 (line, None)
69 };
70
71 let parts: Vec<&str> = adjusted_line.split(',').collect();
72
73 let format = if format.is_empty() {
74 &[
75 "Name",
76 "Fontname",
77 "Fontsize",
78 "PrimaryColour",
79 "SecondaryColour",
80 "OutlineColour",
81 "BackColour",
82 "Bold",
83 "Italic",
84 "Underline",
85 "StrikeOut",
86 "ScaleX",
87 "ScaleY",
88 "Spacing",
89 "Angle",
90 "BorderStyle",
91 "Outline",
92 "Shadow",
93 "Alignment",
94 "MarginL",
95 "MarginR",
96 "MarginV",
97 "Encoding",
98 ]
99 } else {
100 format
101 };
102
103 if parts.len() < format.len() {
104 return Err(ParseError::InsufficientFields {
105 expected: format.len(),
106 found: parts.len(),
107 line: line_number as usize,
108 });
109 }
110
111 let get_field = |name: &str| -> &'a str {
112 format
113 .iter()
114 .position(|&field| field.eq_ignore_ascii_case(name))
115 .and_then(|idx| parts.get(idx))
116 .map_or("", |s| s.trim())
117 };
118
119 let span = Span::new(0, 0, line_number, 1);
121
122 Ok(Style {
123 name: get_field("Name"),
124 parent: parent_style,
125 fontname: get_field("Fontname"),
126 fontsize: get_field("Fontsize"),
127 primary_colour: get_field("PrimaryColour"),
128 secondary_colour: get_field("SecondaryColour"),
129 outline_colour: get_field("OutlineColour"),
130 back_colour: get_field("BackColour"),
131 bold: get_field("Bold"),
132 italic: get_field("Italic"),
133 underline: get_field("Underline"),
134 strikeout: get_field("StrikeOut"),
135 scale_x: get_field("ScaleX"),
136 scale_y: get_field("ScaleY"),
137 spacing: get_field("Spacing"),
138 angle: get_field("Angle"),
139 border_style: get_field("BorderStyle"),
140 outline: get_field("Outline"),
141 shadow: get_field("Shadow"),
142 alignment: get_field("Alignment"),
143 margin_l: get_field("MarginL"),
144 margin_r: get_field("MarginR"),
145 margin_v: get_field("MarginV"),
146 margin_t: format
147 .iter()
148 .any(|&f| f.eq_ignore_ascii_case("MarginT"))
149 .then(|| get_field("MarginT")),
150 margin_b: format
151 .iter()
152 .any(|&f| f.eq_ignore_ascii_case("MarginB"))
153 .then(|| get_field("MarginB")),
154 encoding: get_field("Encoding"),
155 relative_to: format
156 .iter()
157 .any(|&f| f.eq_ignore_ascii_case("RelativeTo"))
158 .then(|| get_field("RelativeTo")),
159 span,
160 })
161 }
162 #[must_use]
170 #[allow(clippy::missing_const_for_fn)] pub fn new(source: &'a str, start_position: usize, start_line: usize) -> Self {
172 Self {
173 tracker: PositionTracker::new_at(
174 source,
175 start_position,
176 u32::try_from(start_line).unwrap_or(u32::MAX),
177 1,
178 ),
179 issues: Vec::new(),
180 format: None,
181 }
182 }
183
184 #[must_use]
186 pub fn with_format(
187 source: &'a str,
188 format: &[&'a str],
189 start_position: usize,
190 start_line: u32,
191 ) -> Self {
192 Self {
193 tracker: PositionTracker::new_at(source, start_position, start_line, 1),
194 issues: Vec::new(),
195 format: Some(format.to_vec()),
196 }
197 }
198
199 pub fn parse(mut self) -> ParseResult<SectionParseResult<'a>> {
213 let mut styles = Vec::new();
214
215 while !self.tracker.is_at_end() && !self.at_next_section() {
216 self.skip_whitespace_and_comments();
217
218 if self.tracker.is_at_end() || self.at_next_section() {
219 break;
220 }
221
222 let line_start = self.tracker.checkpoint();
223 let line = self.current_line().trim();
224
225 if line.is_empty() {
226 self.tracker.skip_line();
227 continue;
228 }
229
230 if line.starts_with("Format:") {
231 self.parse_format_line(line);
232 } else if line.starts_with("Style:") {
233 if let Some(style_data) = line.strip_prefix("Style:") {
234 if let Some(style) =
235 self.parse_style_line_internal(style_data.trim(), &line_start)
236 {
237 styles.push(style);
238 }
239 }
240 }
241
242 self.tracker.skip_line();
243 }
244
245 let format_to_return = if self.format.is_none() && !styles.is_empty() {
247 Some(vec![
248 "Name",
249 "Fontname",
250 "Fontsize",
251 "PrimaryColour",
252 "SecondaryColour",
253 "OutlineColour",
254 "BackColour",
255 "Bold",
256 "Italic",
257 "Underline",
258 "StrikeOut",
259 "ScaleX",
260 "ScaleY",
261 "Spacing",
262 "Angle",
263 "BorderStyle",
264 "Outline",
265 "Shadow",
266 "Alignment",
267 "MarginL",
268 "MarginR",
269 "MarginV",
270 "Encoding",
271 ])
272 } else {
273 self.format
274 };
275
276 Ok((
277 Section::Styles(styles),
278 format_to_return,
279 self.issues,
280 self.tracker.offset(),
281 self.tracker.line() as usize,
282 ))
283 }
284
285 fn parse_format_line(&mut self, line: &'a str) {
287 if let Some(format_data) = line.strip_prefix("Format:") {
288 let fields: Vec<&'a str> = format_data.split(',').map(str::trim).collect();
289 self.format = Some(fields);
290 }
291 }
292
293 fn parse_style_line_internal(
295 &mut self,
296 line: &'a str,
297 line_start: &PositionTracker<'a>,
298 ) -> Option<Style<'a>> {
299 let (adjusted_line, parent_style) = if line.trim_start().starts_with('*') {
301 line.find(',').map_or((line, None), |first_comma| {
303 let parent_part = &line[0..first_comma];
304 let parent_name = parent_part.trim_start().trim_start_matches('*').trim();
305 let remaining = &line[first_comma + 1..];
306 (remaining, Some(parent_name))
307 })
308 } else {
309 (line, None)
310 };
311
312 let parts: Vec<&str> = adjusted_line.split(',').collect();
313
314 let format = self.format.as_deref().unwrap_or(&[
315 "Name",
316 "Fontname",
317 "Fontsize",
318 "PrimaryColour",
319 "SecondaryColour",
320 "OutlineColour",
321 "BackColour",
322 "Bold",
323 "Italic",
324 "Underline",
325 "StrikeOut",
326 "ScaleX",
327 "ScaleY",
328 "Spacing",
329 "Angle",
330 "BorderStyle",
331 "Outline",
332 "Shadow",
333 "Alignment",
334 "MarginL",
335 "MarginR",
336 "MarginV",
337 "Encoding",
338 ]);
339
340 if parts.len() != format.len() {
341 self.issues.push(ParseIssue::new(
342 IssueSeverity::Warning,
343 IssueCategory::Format,
344 format!(
345 "Style line has {} fields, expected {}",
346 parts.len(),
347 format.len()
348 ),
349 line_start.line() as usize,
350 ));
351 if parts.len() < format.len() {
352 return None;
353 }
354 }
355
356 let get_field = |name: &str| -> &'a str {
357 format
358 .iter()
359 .position(|&field| field.eq_ignore_ascii_case(name))
360 .and_then(|idx| parts.get(idx))
361 .map_or("", |s| s.trim())
362 };
363
364 let full_line = self.current_line();
366 let span = line_start.span_for(full_line.len());
367
368 Some(Style {
369 name: get_field("Name"),
370 parent: parent_style,
371 fontname: get_field("Fontname"),
372 fontsize: get_field("Fontsize"),
373 primary_colour: get_field("PrimaryColour"),
374 secondary_colour: get_field("SecondaryColour"),
375 outline_colour: get_field("OutlineColour"),
376 back_colour: get_field("BackColour"),
377 bold: get_field("Bold"),
378 italic: get_field("Italic"),
379 underline: get_field("Underline"),
380 strikeout: get_field("StrikeOut"),
381 scale_x: get_field("ScaleX"),
382 scale_y: get_field("ScaleY"),
383 spacing: get_field("Spacing"),
384 angle: get_field("Angle"),
385 border_style: get_field("BorderStyle"),
386 outline: get_field("Outline"),
387 shadow: get_field("Shadow"),
388 alignment: get_field("Alignment"),
389 margin_l: get_field("MarginL"),
390 margin_r: get_field("MarginR"),
391 margin_v: get_field("MarginV"),
392 margin_t: format
393 .iter()
394 .any(|&f| f.eq_ignore_ascii_case("MarginT"))
395 .then(|| get_field("MarginT")),
396 margin_b: format
397 .iter()
398 .any(|&f| f.eq_ignore_ascii_case("MarginB"))
399 .then(|| get_field("MarginB")),
400 encoding: get_field("Encoding"),
401 relative_to: format
402 .iter()
403 .any(|&f| f.eq_ignore_ascii_case("RelativeTo"))
404 .then(|| get_field("RelativeTo")),
405 span,
406 })
407 }
408
409 fn current_line(&self) -> &'a str {
411 let remaining = self.tracker.remaining();
412 let end = remaining.find('\n').unwrap_or(remaining.len());
413 &remaining[..end]
414 }
415
416 fn at_next_section(&self) -> bool {
418 self.tracker.remaining().trim_start().starts_with('[')
419 }
420
421 fn skip_whitespace_and_comments(&mut self) {
423 loop {
424 self.tracker.skip_whitespace();
425
426 let remaining = self.tracker.remaining();
427 if remaining.is_empty() {
428 break;
429 }
430
431 if remaining.starts_with(';') || remaining.starts_with('#') {
432 self.tracker.skip_line();
433 continue;
434 }
435
436 if remaining.starts_with('\n') {
438 self.tracker.advance(1);
439 continue;
440 }
441
442 break;
443 }
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450 use crate::parser::ast::Section;
451
452 #[test]
453 fn parse_empty_section() {
454 let parser = StylesParser::new("", 0, 1);
455 let result = parser.parse();
456 assert!(result.is_ok());
457 let (section, ..) = result.unwrap();
458 if let Section::Styles(styles) = section {
459 assert!(styles.is_empty());
460 } else {
461 panic!("Expected Styles section");
462 }
463 }
464
465 #[test]
466 fn parse_basic_style() {
467 let content = "Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\nStyle: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,0,0,0,1\n";
468 let parser = StylesParser::new(content, 0, 1);
469 let result = parser.parse();
470 assert!(result.is_ok());
471
472 let (section, ..) = result.unwrap();
473 if let Section::Styles(styles) = section {
474 assert_eq!(styles.len(), 1);
475 let style = &styles[0];
476 assert_eq!(style.name, "Default");
477 assert_eq!(style.fontname, "Arial");
478 assert_eq!(style.fontsize, "20");
479 assert!(style.span.start > 0);
481 assert!(style.span.end > style.span.start);
482 } else {
483 panic!("Expected Styles section");
484 }
485 }
486
487 #[test]
488 fn parse_without_format_line() {
489 let content = "Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,0,0,0,1\n";
490 let parser = StylesParser::new(content, 0, 1);
491 let result = parser.parse();
492 assert!(result.is_ok());
493
494 let (section, ..) = result.unwrap();
495 if let Section::Styles(styles) = section {
496 assert_eq!(styles.len(), 1);
497 assert_eq!(styles[0].name, "Default");
498 } else {
499 panic!("Expected Styles section");
500 }
501 }
502
503 #[test]
504 fn parse_with_inheritance() {
505 let content = "Style: *Default,NewStyle,Arial,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,0,0,2,0,0,0,1\n";
506 let parser = StylesParser::new(content, 0, 1);
507 let result = parser.parse();
508 assert!(result.is_ok());
509
510 let (section, ..) = result.unwrap();
511 if let Section::Styles(styles) = section {
512 assert_eq!(styles.len(), 1);
513 assert_eq!(styles[0].name, "NewStyle");
514 assert_eq!(styles[0].parent, Some("Default"));
515 } else {
516 panic!("Expected Styles section");
517 }
518 }
519
520 #[test]
521 fn parse_with_position_tracking() {
522 let prefix = "a".repeat(100); let section_content = "Style: Test,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,0,0,0,1\n";
525 let full_content = format!("{prefix}{section_content}");
526
527 let parser = StylesParser::new(&full_content, 100, 10);
529 let result = parser.parse();
530 assert!(result.is_ok());
531
532 let (section, _, _, final_pos, final_line) = result.unwrap();
533 if let Section::Styles(styles) = section {
534 assert_eq!(styles.len(), 1);
535 let style = &styles[0];
536 assert_eq!(style.span.start, 100);
537 assert_eq!(style.span.line, 10);
538 } else {
539 panic!("Expected Styles section");
540 }
541
542 assert_eq!(final_pos, 100 + section_content.len());
543 assert_eq!(final_line, 11);
544 }
545
546 #[test]
547 fn test_public_parse_style_line() {
548 let format = vec![
549 "Name",
550 "Fontname",
551 "Fontsize",
552 "PrimaryColour",
553 "SecondaryColour",
554 "OutlineColour",
555 "BackColour",
556 "Bold",
557 "Italic",
558 "Underline",
559 "StrikeOut",
560 "ScaleX",
561 "ScaleY",
562 "Spacing",
563 "Angle",
564 "BorderStyle",
565 "Outline",
566 "Shadow",
567 "Alignment",
568 "MarginL",
569 "MarginR",
570 "MarginV",
571 "Encoding",
572 ];
573 let line = "Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,0,0,0,1";
574
575 let result = StylesParser::parse_style_line(line, &format, 1);
576 assert!(result.is_ok());
577
578 let style = result.unwrap();
579 assert_eq!(style.name, "Default");
580 assert_eq!(style.fontname, "Arial");
581 assert_eq!(style.fontsize, "20");
582 assert!(style.parent.is_none());
583 }
584
585 #[test]
586 fn test_parse_style_line_with_inheritance() {
587 let format = vec![
588 "Name",
589 "Fontname",
590 "Fontsize",
591 "PrimaryColour",
592 "SecondaryColour",
593 "OutlineColour",
594 "BackColour",
595 "Bold",
596 "Italic",
597 "Underline",
598 "StrikeOut",
599 "ScaleX",
600 "ScaleY",
601 "Spacing",
602 "Angle",
603 "BorderStyle",
604 "Outline",
605 "Shadow",
606 "Alignment",
607 "MarginL",
608 "MarginR",
609 "MarginV",
610 "Encoding",
611 ];
612 let line = "*Default,NewStyle,Arial,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,1,0,0,0,100,100,0,0,1,0,0,2,0,0,0,1";
613
614 let result = StylesParser::parse_style_line(line, &format, 1);
615 assert!(result.is_ok());
616
617 let style = result.unwrap();
618 assert_eq!(style.name, "NewStyle");
619 assert_eq!(style.parent, Some("Default"));
620 assert_eq!(style.fontsize, "24");
621 }
622
623 #[test]
624 fn test_parse_style_line_insufficient_fields() {
625 let format = vec![
626 "Name",
627 "Fontname",
628 "Fontsize",
629 "PrimaryColour",
630 "SecondaryColour",
631 "OutlineColour",
632 "BackColour",
633 "Bold",
634 "Italic",
635 "Underline",
636 "StrikeOut",
637 "ScaleX",
638 "ScaleY",
639 "Spacing",
640 "Angle",
641 "BorderStyle",
642 "Outline",
643 "Shadow",
644 "Alignment",
645 "MarginL",
646 "MarginR",
647 "MarginV",
648 "Encoding",
649 ];
650 let line = "Default,Arial,20"; let result = StylesParser::parse_style_line(line, &format, 1);
653 assert!(result.is_err());
654
655 if let Err(e) = result {
656 assert!(matches!(e, ParseError::InsufficientFields { .. }));
657 }
658 }
659
660 #[test]
661 fn test_parse_style_line_with_empty_format() {
662 let format = vec![];
664 let line = "Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,0,0,2,0,0,0,1";
665
666 let result = StylesParser::parse_style_line(line, &format, 1);
667 assert!(result.is_ok());
668
669 let style = result.unwrap();
670 assert_eq!(style.name, "Default");
671 assert_eq!(style.fontname, "Arial");
672 }
673}