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};
20use super::strip_bom;
21
22#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
24pub struct SSAInfo {
25 pub title: Option<String>,
27 pub original_script: Option<String>,
29 pub original_translation: Option<String>,
31 pub original_editing: Option<String>,
33 pub original_timing: Option<String>,
35 pub synch_point: Option<String>,
37 pub script_update_by: Option<String>,
39 pub update_details: Option<String>,
41 pub script_type: Option<String>,
43 pub collisions: Option<String>,
55 pub play_res_y: Option<u32>,
57 pub play_res_x: Option<u32>,
59 pub play_depth: Option<u32>,
61 pub timer: Option<f32>,
63 pub wrap_style: Option<u8>,
70
71 pub additional_fields: HashMap<String, String>,
73}
74impl Eq for SSAInfo {}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct SSAStyle {
81 pub name: String,
83 pub fontname: String,
85 pub fontsize: f32,
87 pub primary_color: Option<Color>,
89 pub secondary_color: Option<Color>,
92 pub outline_color: Option<Color>,
96 pub back_color: Option<Color>,
98 pub bold: bool,
100 pub italic: bool,
102 pub underline: bool,
104 pub strikeout: bool,
106 pub scale_x: f32,
108 pub scale_y: f32,
110 pub spacing: f32,
112 pub angle: f32,
114 pub border_style: u8,
119 pub outline: f32,
123 pub shadow: f32,
127 pub alignment: Alignment,
130 pub margin_l: f32,
132 pub margin_r: f32,
134 pub margin_v: f32,
136 pub encoding: f32,
140}
141impl Eq for SSAStyle {}
142
143impl Default for SSAStyle {
144 fn default() -> Self {
145 SSAStyle {
146 name: "Default".to_string(),
147 fontname: "Trebuchet MS".to_string(),
148 fontsize: 25.5,
149 primary_color: None,
150 secondary_color: None,
151 outline_color: None,
152 back_color: None,
153 bold: false,
154 italic: false,
155 underline: false,
156 strikeout: false,
157 scale_x: 120.0,
158 scale_y: 120.0,
159 spacing: 0.0,
160 angle: 0.0,
161 border_style: 1,
162 outline: 1.0,
163 shadow: 1.0,
164 alignment: Alignment::BottomCenter,
165 margin_l: 0.0,
166 margin_r: 0.0,
167 margin_v: 20.0,
168 encoding: 0.0,
169 }
170 }
171}
172
173#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
174pub enum SSAEventLineType {
175 Dialogue,
176 Comment,
177 Other(String),
178}
179
180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
187pub struct SSAEvent {
188 pub layer: u32,
191 pub start: Time,
193 pub end: Time,
195 pub style: String,
197 pub name: String,
199 pub margin_l: f32,
202 pub margin_r: f32,
205 pub margin_v: f32,
208 pub effect: String,
211 pub text: String,
213 pub line_type: SSAEventLineType,
214}
215impl Eq for SSAEvent {}
216
217impl Default for SSAEvent {
218 fn default() -> Self {
219 SSAEvent {
220 layer: 0,
221 start: Time::from_hms(0, 0, 0).unwrap(),
222 end: Time::from_hms(0, 0, 0).unwrap(),
223 style: "Default".to_string(),
224 name: "".to_string(),
225 margin_l: 0.0,
226 margin_r: 0.0,
227 margin_v: 0.0,
228 effect: "".to_string(),
229 text: "".to_string(),
230 line_type: SSAEventLineType::Dialogue,
231 }
232 }
233}
234#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
236pub struct SSA {
237 pub info: SSAInfo,
238 pub styles: Vec<SSAStyle>,
239 pub events: Vec<SSAEvent>,
240 pub fonts: Vec<String>,
241 pub graphics: Vec<String>,
242}
243
244impl SSA {
245 pub fn parse<S: AsRef<str>>(content: S) -> Result<SSA, SSAError> {
247 let mut blocks = Vec::new();
248 for (i, line) in (1..).zip(strip_bom(&content).lines()) {
249 match line.trim() {
250 l if l.is_empty() || l.starts_with(&[';', '#']) => continue,
251 l if l.starts_with('[') => blocks.push(vec![(i, line)]),
252 _ => {
253 if let Some(b) = blocks.last_mut() {
254 b.push((i, line))
255 }
256 }
257 }
258 }
259
260 if !blocks
261 .first()
262 .map(|b| &b[0])
263 .is_some_and(|l| l.1 == "[Script Info]")
264 {
265 return Err(SSAError::new(SSAErrorKind::Invalid, 1));
266 }
267
268 let mut ssa = SSA::default();
269
270 for block in blocks {
271 let mut iter = block.into_iter();
272 let (i, line) = iter.next().unwrap(); match line {
274 "[Script Info]" => ssa.info = parse::parse_script_info_block(iter)?,
275 "[V4+ Styles]" => ssa.styles = parse::parse_style_block(i, iter)?,
276 "[Events]" => ssa.events = parse::parse_events_block(i, iter)?,
277 "[Fonts]" => ssa.fonts = parse::parse_fonts_block(iter)?,
278 "[Graphics]" => ssa.graphics = parse::parse_graphics_block(iter)?,
279 _ => continue,
280 }
281 }
282
283 Ok(ssa)
284 }
285
286 pub fn to_srt(&self) -> SRT {
297 let style_remove_regex = Regex::new(r"(?m)\{\\.+?}").unwrap();
298
299 let mut lines = vec![];
300
301 for (i, event) in self.events.iter().enumerate() {
302 let mut text = event
303 .text
304 .replace("{\\b1}", "<b>")
305 .replace("{\\b0}", "</b>")
306 .replace("{\\i1}", "<i>")
307 .replace("{\\i0}", "</i>")
308 .replace("{\\u1}", "<u>")
309 .replace("{\\u0}", "</u>")
310 .replace("\\N", "\r\n");
311
312 if !event.style.is_empty() {
313 if let Some(style) = self.styles.iter().find(|s| s.name == event.style) {
314 if style.bold {
315 text = format!("<b>{text}</b>")
316 }
317 if style.italic {
318 text = format!("<i>{text}</i>")
319 }
320 if style.underline {
321 text = format!("<u>{text}</u>")
322 }
323 }
324 }
325
326 lines.push(SRTLine {
327 sequence_number: i as u32 + 1,
328 start: event.start,
329 end: event.end,
330 text: style_remove_regex.replace_all(&text, "").to_string(),
331 })
332 }
333
334 SRT { lines }
335 }
336 pub fn to_vtt(self) -> VTT {
347 self.to_srt().to_vtt()
348 }
349}
350
351impl Display for SSA {
352 #[rustfmt::skip]
353 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
354 let mut lines = vec![];
355
356 lines.push("[Script Info]".to_string());
357 lines.extend(self.info.title.as_ref().map(|l| format!("Title: {l}")));
358 lines.extend(self.info.original_script.as_ref().map(|l| format!("Original Script: {l}")));
359 lines.extend(self.info.original_translation.as_ref().map(|l| format!("Original Translation: {l}")));
360 lines.extend(self.info.original_editing.as_ref().map(|l| format!("Original Editing: {l}")));
361 lines.extend(self.info.original_timing.as_ref().map(|l| format!("Original Timing: {l}")));
362 lines.extend(self.info.synch_point.as_ref().map(|l| format!("Synch Point: {l}")));
363 lines.extend(self.info.script_update_by.as_ref().map(|l| format!("Script Updated By: {l}")));
364 lines.extend(self.info.update_details.as_ref().map(|l| format!("Update Details: {l}")));
365 lines.extend(self.info.script_type.as_ref().map(|l| format!("Script Type: {l}")));
366 lines.extend(self.info.collisions.as_ref().map(|l| format!("Collisions: {l}")));
367 lines.extend(self.info.play_res_y.map(|l| format!("PlayResY: {l}")));
368 lines.extend(self.info.play_res_x.map(|l| format!("PlayResX: {l}")));
369 lines.extend(self.info.play_depth.map(|l| format!("PlayDepth: {l}")));
370 lines.extend(self.info.timer.map(|l| format!("Timer: {l}")));
371 lines.extend(self.info.wrap_style.map(|l| format!("WrapStyle: {l}")));
372 for (k, v) in &self.info.additional_fields {
373 lines.push(format!("{k}: {v}"))
374 }
375
376 lines.push("".to_string());
377 lines.push("[V4+ Styles]".to_string());
378 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());
379 for style in &self.styles {
380 let line = [
381 style.name.to_string(),
382 style.fontname.to_string(),
383 style.fontsize.to_string(),
384 style.primary_color.map(|c| c.to_ssa_string()).unwrap_or_default(),
385 style.secondary_color.map(|c| c.to_ssa_string()).unwrap_or_default(),
386 style.outline_color.map(|c| c.to_ssa_string()).unwrap_or_default(),
387 style.back_color.map(|c| c.to_ssa_string()).unwrap_or_default(),
388 if style.bold { "-1" } else { "0" }.to_string(),
389 if style.italic { "-1" } else { "0" }.to_string(),
390 if style.underline { "-1" } else { "0" }.to_string(),
391 if style.strikeout { "-1" } else { "0" }.to_string(),
392 style.scale_x.to_string(),
393 style.scale_y.to_string(),
394 style.spacing.to_string(),
395 style.angle.to_string(),
396 style.border_style.to_string(),
397 style.outline.to_string(),
398 style.shadow.to_string(),
399 (style.alignment as u8).to_string(),
400 style.margin_l.to_string(),
401 style.margin_r.to_string(),
402 style.margin_v.to_string(),
403 style.encoding.to_string(),
404 ];
405 lines.push(format!("Style: {}", line.join(",")))
406 }
407
408 lines.push("".to_string());
409 lines.push("[Events]".to_string());
410 lines.push("Format: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text".to_string());
411 for event in &self.events {
412 let line = [
413 event.layer.to_string(),
414 event.start.format(TIME_FORMAT).unwrap(),
415 event.end.format(TIME_FORMAT).unwrap(),
416 event.style.to_string(),
417 event.name.to_string(),
418 event.margin_l.to_string(),
419 event.margin_r.to_string(),
420 event.margin_v.to_string(),
421 event.effect.to_string(),
422 event.text.to_string()
423 ];
424 lines.push(format!("Dialogue: {}", line.join(",")))
425 }
426
427 write!(f, "{}", lines.join("\n"))
428 }
429}
430
431error! {
432 SSAError => SSAErrorKind {
433 Invalid,
434 EmptyBlock,
435 Parse(String),
436 MissingHeader(String),
437 }
438}
439
440mod parse {
441 use super::*;
442 use std::num::{ParseFloatError, ParseIntError};
443 use time::format_description::BorrowedFormatItem;
444 use time::macros::format_description;
445
446 pub(super) struct Error {
447 pub(super) line: usize,
448 pub(super) kind: SSAErrorKind,
449 }
450
451 impl From<Error> for SSAError {
452 fn from(e: Error) -> SSAError {
453 SSAError::new(e.kind, e.line)
454 }
455 }
456
457 pub(super) const TIME_FORMAT: &[BorrowedFormatItem] =
458 format_description!("[hour padding:none]:[minute]:[second].[subsecond digits:2]");
459
460 type Result<T> = std::result::Result<T, Error>;
461
462 pub(super) fn parse_script_info_block<'a, I: Iterator<Item = (usize, &'a str)>>(
463 block_lines: I,
464 ) -> Result<SSAInfo> {
465 let mut info = SSAInfo::default();
466
467 for (i, line) in block_lines {
468 let Some((name, mut value)) = line.split_once(':') else {
469 return Err(Error {
470 line: i,
471 kind: SSAErrorKind::Parse("delimiter ':' missing".to_string()),
472 });
473 };
474 value = value.trim();
475
476 if value.is_empty() {
477 continue;
478 }
479
480 match name {
481 "Title" => info.title = Some(value.to_string()),
482 "Original Script" => info.original_script = Some(value.to_string()),
483 "Original Translation" => info.original_translation = Some(value.to_string()),
484 "Original Editing" => info.original_editing = Some(value.to_string()),
485 "Original Timing" => info.original_timing = Some(value.to_string()),
486 "Synch Point" => info.synch_point = Some(value.to_string()),
487 "Script Updated By" => info.script_update_by = Some(value.to_string()),
488 "Update Details" => info.update_details = Some(value.to_string()),
489 "ScriptType" => info.script_type = Some(value.to_string()),
490 "Collisions" => info.collisions = Some(value.to_string()),
491 "PlayResY" => {
492 info.play_res_y = value.parse::<u32>().map(Some).map_err(|e| Error {
493 line: i,
494 kind: SSAErrorKind::Parse(e.to_string()),
495 })?
496 }
497 "PlayResX" => {
498 info.play_res_x = value.parse::<u32>().map(Some).map_err(|e| Error {
499 line: i,
500 kind: SSAErrorKind::Parse(e.to_string()),
501 })?
502 }
503 "PlayDepth" => {
504 info.play_depth = value.parse::<u32>().map(Some).map_err(|e| Error {
505 line: i,
506 kind: SSAErrorKind::Parse(e.to_string()),
507 })?
508 }
509 "Timer" => {
510 info.timer = value.parse::<f32>().map(Some).map_err(|e| Error {
511 line: i,
512 kind: SSAErrorKind::Parse(e.to_string()),
513 })?
514 }
515 "WrapStyle" => {
516 info.wrap_style = value.parse::<u8>().map(Some).map_err(|e| Error {
517 line: i,
518 kind: SSAErrorKind::Parse(e.to_string()),
519 })?
520 }
521 _ => {
522 info.additional_fields
523 .insert(name.to_string(), value.to_string());
524 }
525 }
526 }
527
528 Ok(info)
529 }
530
531 fn parse_block_header<'a, I: Iterator<Item = (usize, &'a str)>>(
532 header_line: usize,
533 mut block_lines: I,
534 ) -> Result<(usize, Vec<&'a str>)> {
535 let (i, line) = block_lines.next().ok_or_else(|| Error {
536 line: header_line,
537 kind: SSAErrorKind::EmptyBlock,
538 })?;
539
540 let header = line.strip_prefix("Format:").ok_or_else(|| Error {
541 line: i,
542 kind: SSAErrorKind::Parse("header must start with 'Format:'".to_string()),
543 })?;
544
545 Ok((i, header.trim().split(',').collect()))
546 }
547
548 pub(super) fn parse_style_block<'a, I: Iterator<Item = (usize, &'a str)>>(
549 header_line: usize,
550 mut block_lines: I,
551 ) -> Result<Vec<SSAStyle>> {
552 let (header_line, headers) = parse_block_header(header_line, &mut block_lines)?;
553
554 let mut styles = vec![];
555
556 for (i, line) in block_lines {
557 let Some(line) = line.strip_prefix("Style:") else {
558 return Err(Error {
559 line: i,
560 kind: SSAErrorKind::Parse("styles line must start with 'Style:'".to_string()),
561 });
562 };
563 let line_list: Vec<&str> = line.trim().split(',').collect();
564
565 styles.push(SSAStyle {
566 name: get_line_value(&headers, "Name", &line_list, header_line, i)?.to_string(),
567 fontname: get_line_value(&headers, "Fontname", &line_list, header_line, i)?
568 .to_string(),
569 fontsize: get_line_value(&headers, "Fontsize", &line_list, header_line, i)?
570 .parse()
571 .map_err(|e| map_parse_float_err(e, i))?,
572 primary_color: Color::from_ssa(get_line_value(
573 &headers,
574 "PrimaryColour",
575 &line_list,
576 header_line,
577 i,
578 )?)
579 .map_err(|e| Error {
580 line: i,
581 kind: SSAErrorKind::Parse(e.to_string()),
582 })?,
583 secondary_color: Color::from_ssa(get_line_value(
584 &headers,
585 "SecondaryColour",
586 &line_list,
587 header_line,
588 i,
589 )?)
590 .map_err(|e| Error {
591 line: i,
592 kind: SSAErrorKind::Parse(e.to_string()),
593 })?,
594 outline_color: Color::from_ssa(get_line_value(
595 &headers,
596 "OutlineColour",
597 &line_list,
598 header_line,
599 i,
600 )?)
601 .map_err(|e| Error {
602 line: i,
603 kind: SSAErrorKind::Parse(e.to_string()),
604 })?,
605 back_color: Color::from_ssa(get_line_value(
606 &headers,
607 "BackColour",
608 &line_list,
609 header_line,
610 i,
611 )?)
612 .map_err(|e| Error {
613 line: i,
614 kind: SSAErrorKind::Parse(e.to_string()),
615 })?,
616 bold: parse_str_to_bool(
617 get_line_value(&headers, "Bold", &line_list, header_line, i)?,
618 i,
619 )?,
620 italic: parse_str_to_bool(
621 get_line_value(&headers, "Italic", &line_list, header_line, i)?,
622 i,
623 )?,
624 underline: parse_str_to_bool(
625 get_line_value(&headers, "Underline", &line_list, header_line, i)?,
626 i,
627 )?,
628 strikeout: parse_str_to_bool(
629 get_line_value(&headers, "StrikeOut", &line_list, header_line, i)?,
630 i,
631 )?,
632 scale_x: get_line_value(&headers, "ScaleX", &line_list, header_line, i)?
633 .parse()
634 .map_err(|e| map_parse_float_err(e, i))?,
635 scale_y: get_line_value(&headers, "ScaleY", &line_list, header_line, i)?
636 .parse()
637 .map_err(|e| map_parse_float_err(e, i))?,
638 spacing: get_line_value(&headers, "Spacing", &line_list, header_line, i)?
639 .parse()
640 .map_err(|e| map_parse_float_err(e, i))?,
641 angle: get_line_value(&headers, "Angle", &line_list, header_line, i)?
642 .parse()
643 .map_err(|e| map_parse_float_err(e, i))?,
644 border_style: get_line_value(&headers, "BorderStyle", &line_list, header_line, i)?
645 .parse()
646 .map_err(|e| map_parse_int_err(e, i))?,
647 outline: get_line_value(&headers, "Outline", &line_list, header_line, i)?
648 .parse()
649 .map(|op: f32| f32::from(op))
650 .map_err(|e| map_parse_float_err(e, i))?,
651 shadow: get_line_value(&headers, "Shadow", &line_list, header_line, i)?
652 .parse()
653 .map(|op: f32| f32::from(op))
654 .map_err(|e| map_parse_float_err(e, i))?,
655 alignment: Alignment::infer_from_str(get_line_value(
656 &headers,
657 "Alignment",
658 &line_list,
659 header_line,
660 i,
661 )?)
662 .unwrap(),
663 margin_l: get_line_value(&headers, "MarginL", &line_list, header_line, i)?
664 .parse()
665 .map(|op: f32| f32::from(op))
666 .map_err(|e| map_parse_float_err(e, i))?,
667 margin_r: get_line_value(&headers, "MarginR", &line_list, header_line, i)?
668 .parse()
669 .map(|op: f32| f32::from(op))
670 .map_err(|e| map_parse_float_err(e, i))?,
671 margin_v: get_line_value(&headers, "MarginV", &line_list, header_line, i)?
672 .parse()
673 .map(|op: f32| f32::from(op))
674 .map_err(|e| map_parse_float_err(e, i))?,
675 encoding: get_line_value(&headers, "Encoding", &line_list, header_line, i)?
676 .parse()
677 .map(|op: f32| f32::from(op))
678 .map_err(|e| map_parse_float_err(e, i))?,
679 })
680 }
681
682 Ok(styles)
683 }
684
685 pub(super) fn parse_events_block<'a, I: Iterator<Item = (usize, &'a str)>>(
686 header_line: usize,
687 mut block_lines: I,
688 ) -> Result<Vec<SSAEvent>> {
689 let (header_line, headers) = parse_block_header(header_line, &mut block_lines)?;
690
691 let mut events = vec![];
692
693 for (i, line) in block_lines {
694 let Some((line_type, line)) = line.split_once(':') else {
695 return Err(Error {
696 line: i,
697 kind: SSAErrorKind::Parse("delimiter ':' missing".to_string()),
698 });
699 };
700 let line_list: Vec<&str> = line.trim().splitn(10, ',').collect();
701
702 events.push(SSAEvent {
703 layer: get_line_value(&headers, "Layer", &line_list, header_line, i)?
704 .parse()
705 .map_err(|e| map_parse_int_err(e, i))?,
706 start: Time::parse(
707 get_line_value(&headers, "Start", &line_list, header_line, i)?,
708 TIME_FORMAT,
709 )
710 .map_err(|e| Error {
711 line: i,
712 kind: SSAErrorKind::Parse(e.to_string()),
713 })?,
714 end: Time::parse(
715 get_line_value(&headers, "End", &line_list, header_line, i)?,
716 TIME_FORMAT,
717 )
718 .map_err(|e| Error {
719 line: i,
720 kind: SSAErrorKind::Parse(e.to_string()),
721 })?,
722 style: get_line_value(&headers, "Style", &line_list, header_line, i)?.to_string(),
723 name: get_line_value(&headers, "Name", &line_list, header_line, i)?.to_string(),
724 margin_l: get_line_value(&headers, "MarginL", &line_list, header_line, i)?
725 .parse()
726 .map_err(|e| map_parse_float_err(e, i))?,
727 margin_r: get_line_value(&headers, "MarginR", &line_list, header_line, i)?
728 .parse()
729 .map_err(|e| map_parse_float_err(e, i))?,
730 margin_v: get_line_value(&headers, "MarginV", &line_list, header_line, i)?
731 .parse()
732 .map_err(|e| map_parse_float_err(e, i))?,
733 effect: get_line_value(&headers, "Effect", &line_list, header_line, i)?.to_string(),
734 text: get_line_value(&headers, "Text", &line_list, header_line, i)?.to_string(),
735 line_type: match line_type {
736 "Dialogue" => SSAEventLineType::Dialogue,
737 "Comment" => SSAEventLineType::Comment,
738 _ => SSAEventLineType::Other(line_type.to_string()),
739 },
740 })
741 }
742
743 Ok(events)
744 }
745
746 pub(super) fn parse_fonts_block<'a, I: Iterator<Item = (usize, &'a str)>>(
747 block_lines: I,
748 ) -> Result<Vec<String>> {
749 let mut fonts = vec![];
750
751 for (i, line) in block_lines {
752 let Some(line) = line.strip_prefix("fontname:") else {
753 return Err(Error {
754 line: i,
755 kind: SSAErrorKind::Parse("fonts line must start with 'fontname:'".to_string()),
756 });
757 };
758 fonts.push(line.trim().to_string())
759 }
760
761 Ok(fonts)
762 }
763
764 pub(super) fn parse_graphics_block<'a, I: Iterator<Item = (usize, &'a str)>>(
765 block_lines: I,
766 ) -> Result<Vec<String>> {
767 let mut graphics = vec![];
768
769 for (i, line) in block_lines {
770 let Some(line) = line.strip_prefix("filename:") else {
771 return Err(Error {
772 line: i,
773 kind: SSAErrorKind::Parse(
774 "graphics line must start with 'filename:'".to_string(),
775 ),
776 });
777 };
778 graphics.push(line.trim().to_string())
779 }
780
781 Ok(graphics)
782 }
783
784 #[allow(clippy::ptr_arg)]
785 fn get_line_value<'a>(
786 headers: &Vec<&str>,
787 name: &str,
788 list: &'a Vec<&str>,
789 header_line: usize,
790 current_line: usize,
791 ) -> Result<&'a str> {
792 let pos = headers
793 .iter()
794 .position(|h| {
795 let value: &str = h.trim();
796
797 value.to_lowercase() == name.to_lowercase()
798 })
799 .ok_or(Error {
800 line: header_line,
801 kind: SSAErrorKind::MissingHeader(name.to_string()),
802 })?;
803
804 list.get(pos).map(|l| l.trim()).ok_or(Error {
805 line: current_line,
806 kind: SSAErrorKind::Parse(format!("no value for header '{}'", name)),
807 })
808 }
809 fn parse_str_to_bool(s: &str, line: usize) -> Result<bool> {
810 match s {
811 "0" => Ok(false),
812 "-1" => Ok(true),
813 _ => Err(Error {
814 line,
815 kind: SSAErrorKind::Parse(
816 "boolean value must be '-1 (true) or '0' (false)".to_string(),
817 ),
818 }),
819 }
820 }
821 fn map_parse_int_err(e: ParseIntError, line: usize) -> Error {
822 Error {
823 line,
824 kind: SSAErrorKind::Parse(e.to_string()),
825 }
826 }
827 fn map_parse_float_err(e: ParseFloatError, line: usize) -> Error {
828 Error {
829 line,
830 kind: SSAErrorKind::Parse(e.to_string()),
831 }
832 }
833}