1use regex::Regex;
7use std::collections::HashMap;
8use std::fmt::Display;
9
10use crate::util::Alignment;
11use serde::{Deserialize, Serialize};
12
13use crate::error;
14use crate::ssa::parse::TIME_FORMAT;
15use crate::util::Color;
16use crate::vtt::VTT;
17use time::Time;
18
19use super::srt::{SRTLine, SRT};
20
21#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
23pub struct SSAInfo {
24 pub title: Option<String>,
26 pub original_script: Option<String>,
28 pub original_translation: Option<String>,
30 pub original_editing: Option<String>,
32 pub original_timing: Option<String>,
34 pub synch_point: Option<String>,
36 pub script_update_by: Option<String>,
38 pub update_details: Option<String>,
40 pub script_type: Option<String>,
42 pub collisions: Option<String>,
54 pub play_res_y: Option<u32>,
56 pub play_res_x: Option<u32>,
58 pub play_depth: Option<u32>,
60 pub timer: Option<f32>,
62 pub wrap_style: Option<u8>,
69
70 pub additional_fields: HashMap<String, String>,
72}
73impl Eq for SSAInfo {}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79pub struct SSAStyle {
80 pub name: String,
82 pub fontname: String,
84 pub fontsize: f32,
86 pub primary_color: Option<Color>,
88 pub secondary_color: Option<Color>,
91 pub outline_color: Option<Color>,
95 pub back_color: Option<Color>,
97 pub bold: bool,
99 pub italic: bool,
101 pub underline: bool,
103 pub strikeout: bool,
105 pub scale_x: f32,
107 pub scale_y: f32,
109 pub spacing: f32,
111 pub angle: f32,
113 pub border_style: u8,
118 pub outline: f32,
122 pub shadow: f32,
126 pub alignment: Alignment,
129 pub margin_l: f32,
131 pub margin_r: f32,
133 pub margin_v: f32,
135 pub encoding: f32,
139}
140impl Eq for SSAStyle {}
141
142impl Default for SSAStyle {
143 fn default() -> Self {
144 SSAStyle {
145 name: "Default".to_string(),
146 fontname: "Trebuchet MS".to_string(),
147 fontsize: 25.5,
148 primary_color: None,
149 secondary_color: None,
150 outline_color: None,
151 back_color: None,
152 bold: false,
153 italic: false,
154 underline: false,
155 strikeout: false,
156 scale_x: 120.0,
157 scale_y: 120.0,
158 spacing: 0.0,
159 angle: 0.0,
160 border_style: 1,
161 outline: 1.0,
162 shadow: 1.0,
163 alignment: Alignment::BottomCenter,
164 margin_l: 0.0,
165 margin_r: 0.0,
166 margin_v: 20.0,
167 encoding: 0.0,
168 }
169 }
170}
171
172#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
173pub enum SSAEventLineType {
174 Dialogue,
175 Comment,
176 Other(String),
177}
178
179#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
186pub struct SSAEvent {
187 pub layer: u32,
190 pub start: Time,
192 pub end: Time,
194 pub style: String,
196 pub name: String,
198 pub margin_l: f32,
201 pub margin_r: f32,
204 pub margin_v: f32,
207 pub effect: String,
210 pub text: String,
212 pub line_type: SSAEventLineType,
213}
214impl Eq for SSAEvent {}
215
216impl Default for SSAEvent {
217 fn default() -> Self {
218 SSAEvent {
219 layer: 0,
220 start: Time::from_hms(0, 0, 0).unwrap(),
221 end: Time::from_hms(0, 0, 0).unwrap(),
222 style: "Default".to_string(),
223 name: "".to_string(),
224 margin_l: 0.0,
225 margin_r: 0.0,
226 margin_v: 0.0,
227 effect: "".to_string(),
228 text: "".to_string(),
229 line_type: SSAEventLineType::Dialogue,
230 }
231 }
232}
233#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
235pub struct SSA {
236 pub info: SSAInfo,
237 pub styles: Vec<SSAStyle>,
238 pub events: Vec<SSAEvent>,
239 pub fonts: Vec<String>,
240 pub graphics: Vec<String>,
241}
242
243impl SSA {
244 pub fn parse<S: AsRef<str>>(content: S) -> Result<SSA, SSAError> {
246 let mut line_num = 0;
247
248 let mut blocks = vec![vec![]];
249 for line in content.as_ref().lines() {
250 if line.trim().is_empty() {
251 blocks.push(vec![])
252 } else {
253 blocks.last_mut().unwrap().push(line)
254 }
255 }
256
257 let mut ssa = SSA::default();
258
259 if blocks[0].first().is_some_and(|l| *l == "[Script Info]") {
260 line_num += 1;
261 let mut block = blocks.remove(0);
262 let block_len = block.len();
263 block.remove(0);
264 ssa.info = parse::parse_script_info_block(block.into_iter())
265 .map_err(|e| SSAError::new(e.kind, line_num + e.line))?;
266 line_num += block_len
267 } else {
268 return Err(SSAError::new(SSAErrorKind::Invalid, 1));
269 }
270
271 for mut block in blocks {
272 line_num += 1;
273
274 if block.is_empty() {
275 return Err(SSAError::new(SSAErrorKind::EmptyBlock, line_num));
276 }
277
278 let block_len = block.len();
279
280 match block.remove(0) {
281 "[V4+ Styles]" => {
282 ssa.styles = parse::parse_style_block(block.into_iter())
283 .map_err(|e| SSAError::new(e.kind, line_num + e.line))?
284 }
285 "[Events]" => {
286 ssa.events = parse::parse_events_block(block.into_iter())
287 .map_err(|e| SSAError::new(e.kind, line_num + e.line))?
288 }
289 "[Fonts]" => {
290 ssa.fonts = parse::parse_fonts_block(block.into_iter())
291 .map_err(|e| SSAError::new(e.kind, line_num + e.line))?
292 }
293 "[Graphics]" => {
294 ssa.graphics = parse::parse_graphics_block(block.into_iter())
295 .map_err(|e| SSAError::new(e.kind, line_num + e.line))?
296 }
297 _ => continue,
298 }
299
300 line_num += block_len
301 }
302
303 Ok(ssa)
304 }
305
306 pub fn to_srt(&self) -> SRT {
317 let style_remove_regex = Regex::new(r"(?m)\{\\.+?}").unwrap();
318
319 let mut lines = vec![];
320
321 for (i, event) in self.events.iter().enumerate() {
322 let mut text = event
323 .text
324 .replace("{\\b1}", "<b>")
325 .replace("{\\b0}", "</b>")
326 .replace("{\\i1}", "<i>")
327 .replace("{\\i0}", "</i>")
328 .replace("{\\u1}", "<u>")
329 .replace("{\\u0}", "</u>")
330 .replace("\\N", "\r\n");
331
332 if !event.style.is_empty() {
333 if let Some(style) = self.styles.iter().find(|s| s.name == event.style) {
334 if style.bold {
335 text = format!("<b>{text}</b>")
336 }
337 if style.italic {
338 text = format!("<i>{text}</i>")
339 }
340 if style.underline {
341 text = format!("<u>{text}</u>")
342 }
343 }
344 }
345
346 lines.push(SRTLine {
347 sequence_number: i as u32 + 1,
348 start: event.start,
349 end: event.end,
350 text: style_remove_regex.replace_all(&text, "").to_string(),
351 })
352 }
353
354 SRT { lines }
355 }
356 pub fn to_vtt(self) -> VTT {
367 self.to_srt().to_vtt()
368 }
369}
370
371impl Display for SSA {
372 #[rustfmt::skip]
373 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374 let mut lines = vec![];
375
376 lines.push("[Script Info]".to_string());
377 lines.extend(self.info.title.as_ref().map(|l| format!("Title: {l}")));
378 lines.extend(self.info.original_script.as_ref().map(|l| format!("Original Script: {l}")));
379 lines.extend(self.info.original_translation.as_ref().map(|l| format!("Original Translation: {l}")));
380 lines.extend(self.info.original_editing.as_ref().map(|l| format!("Original Editing: {l}")));
381 lines.extend(self.info.original_timing.as_ref().map(|l| format!("Original Timing: {l}")));
382 lines.extend(self.info.synch_point.as_ref().map(|l| format!("Synch Point: {l}")));
383 lines.extend(self.info.script_update_by.as_ref().map(|l| format!("Script Updated By: {l}")));
384 lines.extend(self.info.update_details.as_ref().map(|l| format!("Update Details: {l}")));
385 lines.extend(self.info.script_type.as_ref().map(|l| format!("Script Type: {l}")));
386 lines.extend(self.info.collisions.as_ref().map(|l| format!("Collisions: {l}")));
387 lines.extend(self.info.play_res_y.map(|l| format!("PlayResY: {l}")));
388 lines.extend(self.info.play_res_x.map(|l| format!("PlayResX: {l}")));
389 lines.extend(self.info.play_depth.map(|l| format!("PlayDepth: {l}")));
390 lines.extend(self.info.timer.map(|l| format!("Timer: {l}")));
391 lines.extend(self.info.wrap_style.map(|l| format!("WrapStyle: {l}")));
392 for (k, v) in &self.info.additional_fields {
393 lines.push(format!("{k}: {v}"))
394 }
395
396 lines.push("".to_string());
397 lines.push("[V4+ Styles]".to_string());
398 lines.push("Format: Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,MarginL,MarginR,MarginV,Encoding".to_string());
399 for style in &self.styles {
400 let line = [
401 style.name.to_string(),
402 style.fontname.to_string(),
403 style.fontsize.to_string(),
404 style.primary_color.map(|c| c.to_ssa_string()).unwrap_or_default(),
405 style.secondary_color.map(|c| c.to_ssa_string()).unwrap_or_default(),
406 style.outline_color.map(|c| c.to_ssa_string()).unwrap_or_default(),
407 style.back_color.map(|c| c.to_ssa_string()).unwrap_or_default(),
408 if style.bold { "-1" } else { "0" }.to_string(),
409 if style.italic { "-1" } else { "0" }.to_string(),
410 if style.underline { "-1" } else { "0" }.to_string(),
411 if style.strikeout { "-1" } else { "0" }.to_string(),
412 style.scale_x.to_string(),
413 style.scale_y.to_string(),
414 style.spacing.to_string(),
415 style.angle.to_string(),
416 style.border_style.to_string(),
417 style.outline.to_string(),
418 style.shadow.to_string(),
419 (style.alignment as u8).to_string(),
420 style.margin_l.to_string(),
421 style.margin_r.to_string(),
422 style.margin_v.to_string(),
423 style.encoding.to_string(),
424 ];
425 lines.push(format!("Style: {}", line.join(",")))
426 }
427
428 lines.push("".to_string());
429 lines.push("[Events]".to_string());
430 lines.push("Format: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text".to_string());
431 for event in &self.events {
432 let line = [
433 event.layer.to_string(),
434 event.start.format(TIME_FORMAT).unwrap(),
435 event.end.format(TIME_FORMAT).unwrap(),
436 event.style.to_string(),
437 event.name.to_string(),
438 event.margin_l.to_string(),
439 event.margin_r.to_string(),
440 event.margin_v.to_string(),
441 event.effect.to_string(),
442 event.text.to_string()
443 ];
444 lines.push(format!("Dialogue: {}", line.join(",")))
445 }
446
447 write!(f, "{}", lines.join("\n"))
448 }
449}
450
451error! {
452 SSAError => SSAErrorKind {
453 Invalid,
454 EmptyBlock,
455 Parse(String),
456 MissingHeader(String),
457 }
458}
459
460mod parse {
461 use super::*;
462 use std::num::{ParseFloatError, ParseIntError};
463 use time::format_description::BorrowedFormatItem;
464 use time::macros::format_description;
465
466 pub(super) struct Error {
467 pub(super) line: usize,
468 pub(super) kind: SSAErrorKind,
469 }
470
471 pub(super) const TIME_FORMAT: &[BorrowedFormatItem] =
472 format_description!("[hour padding:none]:[minute]:[second].[subsecond digits:2]");
473
474 type Result<T> = std::result::Result<T, Error>;
475
476 pub(super) fn parse_script_info_block<'a, I: Iterator<Item = &'a str>>(
477 block_lines: I,
478 ) -> Result<SSAInfo> {
479 let mut info = SSAInfo::default();
480
481 for (i, line) in block_lines.enumerate() {
482 if line.starts_with(';') {
483 continue;
484 }
485
486 let Some((name, mut value)) = line.split_once(':') else {
487 return Err(Error {
488 line: 1 + i,
489 kind: SSAErrorKind::Parse("delimiter ':' missing".to_string()),
490 });
491 };
492 value = value.trim();
493
494 if value.is_empty() {
495 continue;
496 }
497
498 match name {
499 "Title" => info.title = Some(value.to_string()),
500 "Original Script" => info.original_script = Some(value.to_string()),
501 "Original Translation" => info.original_translation = Some(value.to_string()),
502 "Original Editing" => info.original_editing = Some(value.to_string()),
503 "Original Timing" => info.original_timing = Some(value.to_string()),
504 "Synch Point" => info.synch_point = Some(value.to_string()),
505 "Script Updated By" => info.script_update_by = Some(value.to_string()),
506 "Update Details" => info.update_details = Some(value.to_string()),
507 "ScriptType" => info.script_type = Some(value.to_string()),
508 "Collisions" => info.collisions = Some(value.to_string()),
509 "PlayResY" => {
510 info.play_res_y = value.parse::<u32>().map(Some).map_err(|e| Error {
511 line: 1 + i,
512 kind: SSAErrorKind::Parse(e.to_string()),
513 })?
514 }
515 "PlayResX" => {
516 info.play_res_x = value.parse::<u32>().map(Some).map_err(|e| Error {
517 line: 1 + i,
518 kind: SSAErrorKind::Parse(e.to_string()),
519 })?
520 }
521 "PlayDepth" => {
522 info.play_depth = value.parse::<u32>().map(Some).map_err(|e| Error {
523 line: 1 + i,
524 kind: SSAErrorKind::Parse(e.to_string()),
525 })?
526 }
527 "Timer" => {
528 info.timer = value.parse::<f32>().map(Some).map_err(|e| Error {
529 line: 1 + i,
530 kind: SSAErrorKind::Parse(e.to_string()),
531 })?
532 }
533 "WrapStyle" => {
534 info.wrap_style = value.parse::<u8>().map(Some).map_err(|e| Error {
535 line: 1 + i,
536 kind: SSAErrorKind::Parse(e.to_string()),
537 })?
538 }
539 _ => {
540 info.additional_fields
541 .insert(name.to_string(), value.to_string());
542 }
543 }
544 }
545
546 Ok(info)
547 }
548
549 pub(super) fn parse_style_block<'a, I: Iterator<Item = &'a str>>(
550 mut block_lines: I,
551 ) -> Result<Vec<SSAStyle>> {
552 let mut header_line = 1;
553 let header = loop {
554 let Some(line) = block_lines.next() else {
555 return Err(Error {
556 line: 1,
557 kind: SSAErrorKind::EmptyBlock,
558 });
559 };
560 if !line.starts_with(';') {
561 break line.to_string();
562 }
563 header_line += 1;
564 };
565 let Some(header) = header.strip_prefix("Format:") else {
566 return Err(Error {
567 line: header_line,
568 kind: SSAErrorKind::Parse("styles header must start with 'Format:'".to_string()),
569 });
570 };
571 let headers = header.trim().split(',').collect();
572
573 let mut styles = vec![];
574
575 for (i, line) in block_lines.enumerate() {
576 if line.starts_with(';') {
577 continue;
578 }
579
580 let Some(line) = line.strip_prefix("Style:") else {
581 return Err(Error {
582 line: header_line + 1 + i,
583 kind: SSAErrorKind::Parse("styles line must start with 'Style:'".to_string()),
584 });
585 };
586 let line_list: Vec<&str> = line.trim().split(',').collect();
587
588 styles.push(SSAStyle {
589 name: get_line_value(
590 &headers,
591 "Name",
592 &line_list,
593 header_line,
594 header_line + 1 + i,
595 )?
596 .to_string(),
597 fontname: get_line_value(
598 &headers,
599 "Fontname",
600 &line_list,
601 header_line,
602 header_line + 1 + i,
603 )?
604 .to_string(),
605 fontsize: get_line_value(
606 &headers,
607 "Fontsize",
608 &line_list,
609 header_line,
610 header_line + 1 + i,
611 )?
612 .parse()
613 .map_err(|e| map_parse_float_err(e, header_line + 1 + i))?,
614 primary_color: Color::from_ssa(get_line_value(
615 &headers,
616 "PrimaryColour",
617 &line_list,
618 header_line,
619 header_line + 1 + i,
620 )?)
621 .map_err(|e| Error {
622 line: 2 + i,
623 kind: SSAErrorKind::Parse(e.to_string()),
624 })?,
625 secondary_color: Color::from_ssa(get_line_value(
626 &headers,
627 "SecondaryColour",
628 &line_list,
629 header_line,
630 header_line + 1 + i,
631 )?)
632 .map_err(|e| Error {
633 line: 2 + i,
634 kind: SSAErrorKind::Parse(e.to_string()),
635 })?,
636 outline_color: Color::from_ssa(get_line_value(
637 &headers,
638 "OutlineColour",
639 &line_list,
640 header_line,
641 header_line + 1 + i,
642 )?)
643 .map_err(|e| Error {
644 line: 2 + i,
645 kind: SSAErrorKind::Parse(e.to_string()),
646 })?,
647 back_color: Color::from_ssa(get_line_value(
648 &headers,
649 "BackColour",
650 &line_list,
651 header_line,
652 header_line + 1 + i,
653 )?)
654 .map_err(|e| Error {
655 line: header_line + 1 + i,
656 kind: SSAErrorKind::Parse(e.to_string()),
657 })?,
658 bold: parse_str_to_bool(
659 get_line_value(
660 &headers,
661 "Bold",
662 &line_list,
663 header_line,
664 header_line + 1 + i,
665 )?,
666 header_line + 1 + i,
667 )?,
668 italic: parse_str_to_bool(
669 get_line_value(
670 &headers,
671 "Italic",
672 &line_list,
673 header_line,
674 header_line + 1 + i,
675 )?,
676 header_line + 1 + i,
677 )?,
678 underline: parse_str_to_bool(
679 get_line_value(
680 &headers,
681 "Underline",
682 &line_list,
683 header_line,
684 header_line + 1 + i,
685 )?,
686 header_line + 1 + i,
687 )?,
688 strikeout: parse_str_to_bool(
689 get_line_value(
690 &headers,
691 "StrikeOut",
692 &line_list,
693 header_line,
694 header_line + 1 + i,
695 )?,
696 header_line + 1 + i,
697 )?,
698 scale_x: get_line_value(
699 &headers,
700 "ScaleX",
701 &line_list,
702 header_line,
703 header_line + 1 + i,
704 )?
705 .parse()
706 .map_err(|e| map_parse_float_err(e, header_line + 1 + i))?,
707 scale_y: get_line_value(
708 &headers,
709 "ScaleY",
710 &line_list,
711 header_line,
712 header_line + 1 + i,
713 )?
714 .parse()
715 .map_err(|e| map_parse_float_err(e, header_line + 1 + i))?,
716 spacing: get_line_value(
717 &headers,
718 "Spacing",
719 &line_list,
720 header_line,
721 header_line + 1 + i,
722 )?
723 .parse()
724 .map_err(|e| map_parse_float_err(e, header_line + 1 + i))?,
725 angle: get_line_value(
726 &headers,
727 "Angle",
728 &line_list,
729 header_line,
730 header_line + 1 + i,
731 )?
732 .parse()
733 .map_err(|e| map_parse_float_err(e, header_line + 1 + i))?,
734 border_style: get_line_value(
735 &headers,
736 "BorderStyle",
737 &line_list,
738 header_line,
739 header_line + 1 + i,
740 )?
741 .parse()
742 .map_err(|e| map_parse_int_err(e, header_line + 1 + i))?,
743 outline: get_line_value(
744 &headers,
745 "Outline",
746 &line_list,
747 header_line,
748 header_line + 1 + i,
749 )?
750 .parse()
751 .map(|op: f32| f32::from(op))
752 .map_err(|e| map_parse_float_err(e, header_line + 1 + i))?,
753 shadow: get_line_value(
754 &headers,
755 "Shadow",
756 &line_list,
757 header_line,
758 header_line + 1 + i,
759 )?
760 .parse()
761 .map(|op: f32| f32::from(op))
762 .map_err(|e| map_parse_float_err(e, header_line + 1 + i))?,
763 alignment: Alignment::infer_from_str(get_line_value(
764 &headers,
765 "Alignment",
766 &line_list,
767 header_line,
768 header_line + 1 + i,
769 )?)
770 .unwrap(),
771 margin_l: get_line_value(
772 &headers,
773 "MarginL",
774 &line_list,
775 header_line,
776 header_line + 1 + i,
777 )?
778 .parse()
779 .map(|op: f32| f32::from(op))
780 .map_err(|e| map_parse_float_err(e, header_line + 1 + i))?,
781 margin_r: get_line_value(
782 &headers,
783 "MarginR",
784 &line_list,
785 header_line,
786 header_line + 1 + i,
787 )?
788 .parse()
789 .map(|op: f32| f32::from(op))
790 .map_err(|e| map_parse_float_err(e, header_line + 1 + i))?,
791 margin_v: get_line_value(
792 &headers,
793 "MarginV",
794 &line_list,
795 header_line,
796 header_line + 1 + i,
797 )?
798 .parse()
799 .map(|op: f32| f32::from(op))
800 .map_err(|e| map_parse_float_err(e, header_line + 1 + i))?,
801 encoding: get_line_value(
802 &headers,
803 "Encoding",
804 &line_list,
805 header_line,
806 header_line + 1 + i,
807 )?
808 .parse()
809 .map(|op: f32| f32::from(op))
810 .map_err(|e| map_parse_float_err(e, header_line + 1 + i))?,
811 })
812 }
813
814 Ok(styles)
815 }
816
817 pub(super) fn parse_events_block<'a, I: Iterator<Item = &'a str>>(
818 mut block_lines: I,
819 ) -> Result<Vec<SSAEvent>> {
820 let mut header_line = 1;
821 let header = loop {
822 let Some(line) = block_lines.next() else {
823 return Err(Error {
824 line: 1,
825 kind: SSAErrorKind::EmptyBlock,
826 });
827 };
828 if !line.starts_with(';') {
829 break line.to_string();
830 }
831 header_line += 1;
832 };
833 let Some(header) = header.strip_prefix("Format:") else {
834 return Err(Error {
835 line: header_line,
836 kind: SSAErrorKind::Parse("events header must start with 'Format:'".to_string()),
837 });
838 };
839 let headers = header.trim().split(',').collect();
840
841 let mut events = vec![];
842
843 for (i, line) in block_lines.enumerate() {
844 if line.starts_with(';') {
845 continue;
846 }
847
848 let Some((line_type, line)) = line.split_once(':') else {
849 return Err(Error {
850 line: 2 + i,
851 kind: SSAErrorKind::Parse("delimiter ':' missing".to_string()),
852 });
853 };
854 let line_list: Vec<&str> = line.trim().splitn(10, ',').collect();
855
856 events.push(SSAEvent {
857 layer: get_line_value(
858 &headers,
859 "Layer",
860 &line_list,
861 header_line,
862 header_line + 1 + i,
863 )?
864 .parse()
865 .map_err(|e| map_parse_int_err(e, header_line + 1 + i))?,
866 start: Time::parse(
867 get_line_value(
868 &headers,
869 "Start",
870 &line_list,
871 header_line,
872 header_line + 1 + i,
873 )?,
874 TIME_FORMAT,
875 )
876 .map_err(|e| Error {
877 line: header_line + 1 + i,
878 kind: SSAErrorKind::Parse(e.to_string()),
879 })?,
880 end: Time::parse(
881 get_line_value(
882 &headers,
883 "End",
884 &line_list,
885 header_line,
886 header_line + 1 + i,
887 )?,
888 TIME_FORMAT,
889 )
890 .map_err(|e| Error {
891 line: header_line + 1 + i,
892 kind: SSAErrorKind::Parse(e.to_string()),
893 })?,
894 style: get_line_value(
895 &headers,
896 "Style",
897 &line_list,
898 header_line,
899 header_line + 1 + i,
900 )?
901 .to_string(),
902 name: get_line_value(
903 &headers,
904 "Name",
905 &line_list,
906 header_line,
907 header_line + 1 + i,
908 )?
909 .to_string(),
910 margin_l: get_line_value(
911 &headers,
912 "MarginL",
913 &line_list,
914 header_line,
915 header_line + 1 + i,
916 )?
917 .parse()
918 .map_err(|e| map_parse_float_err(e, header_line + 1 + i))?,
919 margin_r: get_line_value(
920 &headers,
921 "MarginR",
922 &line_list,
923 header_line,
924 header_line + 1 + i,
925 )?
926 .parse()
927 .map_err(|e| map_parse_float_err(e, header_line + 1 + i))?,
928 margin_v: get_line_value(
929 &headers,
930 "MarginV",
931 &line_list,
932 header_line,
933 header_line + 1 + i,
934 )?
935 .parse()
936 .map_err(|e| map_parse_float_err(e, header_line + 1 + i))?,
937 effect: get_line_value(
938 &headers,
939 "Effect",
940 &line_list,
941 header_line,
942 header_line + 1 + i,
943 )?
944 .to_string(),
945 text: get_line_value(
946 &headers,
947 "Text",
948 &line_list,
949 header_line,
950 header_line + 1 + i,
951 )?
952 .to_string(),
953 line_type: match line_type {
954 "Dialogue" => SSAEventLineType::Dialogue,
955 "Comment" => SSAEventLineType::Comment,
956 _ => SSAEventLineType::Other(line_type.to_string()),
957 },
958 })
959 }
960
961 Ok(events)
962 }
963
964 pub(super) fn parse_fonts_block<'a, I: Iterator<Item = &'a str>>(
965 block_lines: I,
966 ) -> Result<Vec<String>> {
967 let mut fonts = vec![];
968
969 for (i, line) in block_lines.enumerate() {
970 let Some(line) = line.strip_prefix("fontname:") else {
971 return Err(Error {
972 line: 1 + i,
973 kind: SSAErrorKind::Parse("fonts line must start with 'fontname:'".to_string()),
974 });
975 };
976 fonts.push(line.trim().to_string())
977 }
978
979 Ok(fonts)
980 }
981
982 pub(super) fn parse_graphics_block<'a, I: Iterator<Item = &'a str>>(
983 block_lines: I,
984 ) -> Result<Vec<String>> {
985 let mut graphics = vec![];
986
987 for (i, line) in block_lines.enumerate() {
988 let Some(line) = line.strip_prefix("filename:") else {
989 return Err(Error {
990 line: 1 + i,
991 kind: SSAErrorKind::Parse(
992 "graphics line must start with 'filename:'".to_string(),
993 ),
994 });
995 };
996 graphics.push(line.trim().to_string())
997 }
998
999 Ok(graphics)
1000 }
1001
1002 #[allow(clippy::ptr_arg)]
1003 fn get_line_value<'a>(
1004 headers: &Vec<&str>,
1005 name: &str,
1006 list: &'a Vec<&str>,
1007 header_line: usize,
1008 current_line: usize,
1009 ) -> Result<&'a &'a str> {
1010 let pos = headers
1011 .iter()
1012 .position(|h| {
1013 let value: &str = h.trim();
1014
1015 value.to_lowercase() == name.to_lowercase()
1016 })
1017 .ok_or(Error {
1018 line: header_line,
1019 kind: SSAErrorKind::MissingHeader(name.to_string()),
1020 })?;
1021
1022 list.get(pos).ok_or(Error {
1023 line: current_line,
1024 kind: SSAErrorKind::Parse(format!("no value for header '{}'", name)),
1025 })
1026 }
1027 fn parse_str_to_bool(s: &str, line: usize) -> Result<bool> {
1028 match s {
1029 "0" => Ok(false),
1030 "-1" => Ok(true),
1031 _ => Err(Error {
1032 line,
1033 kind: SSAErrorKind::Parse(
1034 "boolean value must be '-1 (true) or '0' (false)".to_string(),
1035 ),
1036 }),
1037 }
1038 }
1039 fn map_parse_int_err(e: ParseIntError, line: usize) -> Error {
1040 Error {
1041 line,
1042 kind: SSAErrorKind::Parse(e.to_string()),
1043 }
1044 }
1045 fn map_parse_float_err(e: ParseFloatError, line: usize) -> Error {
1046 Error {
1047 line,
1048 kind: SSAErrorKind::Parse(e.to_string()),
1049 }
1050 }
1051}