use self::errors::*;
use self::errors::ErrorKind::*;
use {SubtitleEntry, SubtitleFile};
use combine::char::{char, string};
use combine::combinator::{eof, parser as p, skip_many};
use combine::primitives::Parser;
use errors::Result as SubtitleParserResult;
use formats::common::*;
use itertools::Itertools;
use std::iter::once;
use timetypes::{TimePoint, TimeSpan};
#[allow(missing_docs)]
pub mod errors {
error_chain! {
errors {
ExpectedIndexLine(line: String) {
display("expected SubRip index line, found '{}'", line)
}
ExpectedTimestampLine(line: String) {
display("expected SubRip timespan line, found '{}'", line)
}
ErrorAtLine(line_num: usize) {
display("parse error at line `{}`", line_num)
}
}
}
}
enum SrtParserState {
Emptyline,
Index(i64),
Timing(i64, TimeSpan),
Dialog(i64, TimeSpan, Vec<String>),
}
#[derive(Debug, Clone)]
pub struct SrtFile {
v: Vec<SrtLine>,
}
#[derive(Debug, Clone)]
struct SrtLine {
timespan: TimeSpan,
index: i64,
texts: Vec<String>,
}
impl SrtFile {
pub fn parse(s: &str) -> SubtitleParserResult<SrtFile> {
let file_opt = Self::parse_file(s);
match file_opt {
Ok(file) => Ok(file),
Err(err) => Err(err.into()),
}
}
}
impl SrtFile {
fn parse_file(i: &str) -> Result<SrtFile> {
use self::SrtParserState::*;
let mut result: Vec<SrtLine> = Vec::new();
let (_, s) = split_bom(i);
let mut state: SrtParserState = Emptyline;
for (line_num, line) in s.lines().chain(once("")).enumerate() {
state = match state {
Emptyline => {
if line.trim().is_empty() {
Emptyline
} else {
Index(Self::parse_index_line(line_num, line)?)
}
}
Index(index) => Timing(index, Self::parse_timespan_line(line_num, line)?),
Timing(index, timespan) => Self::state_expect_dialog(line, &mut result, index, timespan, Vec::new()),
Dialog(index, timespan, texts) => Self::state_expect_dialog(line, &mut result, index, timespan, texts),
};
}
Ok(SrtFile { v: result })
}
fn state_expect_dialog(line: &str, result: &mut Vec<SrtLine>, index: i64, timespan: TimeSpan, mut texts: Vec<String>) -> SrtParserState {
if line.trim().is_empty() {
result.push(SrtLine {
index: index,
timespan: timespan,
texts: texts,
});
SrtParserState::Emptyline
} else {
texts.push(line.trim().to_string());
SrtParserState::Dialog(index, timespan, texts)
}
}
fn parse_index_line(line_num: usize, s: &str) -> Result<i64> {
s.trim()
.parse::<i64>()
.chain_err(|| ExpectedIndexLine(s.to_string()))
.chain_err(|| ErrorAtLine(line_num))
}
fn parse_timespan_line(line_num: usize, line: &str) -> Result<TimeSpan> {
let timestamp = |s| {
(
p(number_i64),
char(':'),
p(number_i64),
char(':'),
p(number_i64),
char(','),
p(number_i64),
)
.map(|t| TimePoint::from_components(t.0, t.2, t.4, t.6))
.parse_stream(s)
};
(
skip_many(ws()),
p(×tamp),
skip_many(ws()),
string("-->"),
skip_many(ws()),
p(×tamp),
skip_many(ws()),
eof(),
)
.map(|t| TimeSpan::new(t.1, t.5))
.parse(line)
.map(|x| x.0)
.map_err(|_| Error::from(ExpectedTimestampLine(line.to_string())))
.chain_err(|| ErrorAtLine(line_num))
}
}
impl SubtitleFile for SrtFile {
fn get_subtitle_entries(&self) -> SubtitleParserResult<Vec<SubtitleEntry>> {
let timings = self.v
.iter()
.map(|line| {
SubtitleEntry::new(line.timespan, line.texts.iter().join("\n"))
})
.collect();
Ok(timings)
}
fn update_subtitle_entries(&mut self, new_subtitle_entries: &[SubtitleEntry]) -> SubtitleParserResult<()> {
assert_eq!(self.v.len(), new_subtitle_entries.len());
for (line_ref, new_entry_ref) in self.v.iter_mut().zip(new_subtitle_entries) {
line_ref.timespan = new_entry_ref.timespan;
if let Some(ref text) = new_entry_ref.line {
line_ref.texts = text.lines().map(str::to_string).collect();
}
}
Ok(())
}
fn to_data(&self) -> SubtitleParserResult<Vec<u8>> {
let timepoint_to_str = |t: TimePoint| -> String {
format!(
"{:02}:{:02}:{:02},{:03}",
t.hours(),
t.mins_comp(),
t.secs_comp(),
t.msecs_comp()
)
};
let line_to_str = |line: &SrtLine| -> String {
format!(
"{}\n{} --> {}\n{}\n\n",
line.index,
timepoint_to_str(line.timespan.start),
timepoint_to_str(line.timespan.end),
line.texts.join("\n")
)
};
Ok(
self.v
.iter()
.map(line_to_str)
.collect::<String>()
.into_bytes(),
)
}
}
impl SrtFile {
pub fn create(v: Vec<(TimeSpan, String)>) -> SubtitleParserResult<SrtFile> {
let file_parts = v.into_iter()
.enumerate()
.map(|(i, (ts, text))| {
SrtLine {
index: i as i64 + 1,
timespan: ts,
texts: text.lines().map(str::to_string).collect(),
}
})
.collect();
Ok(SrtFile { v: file_parts })
}
}
#[cfg(test)]
mod tests {
#[test]
fn create_srt_test() {
use timetypes::{TimePoint, TimeSpan};
use SubtitleFile;
let lines = vec![
(
TimeSpan::new(TimePoint::from_msecs(1500), TimePoint::from_msecs(3700)),
"line1".to_string()
),
(
TimeSpan::new(TimePoint::from_msecs(4500), TimePoint::from_msecs(8700)),
"line2".to_string()
),
];
let file = super::SrtFile::create(lines).unwrap();
let data_string = String::from_utf8(file.to_data().unwrap()).unwrap();
let expected = "1\n00:00:01,500 --> 00:00:03,700\nline1\n\n2\n00:00:04,500 --> 00:00:08,700\nline2\n\n".to_string();
println!("\n{:?}\n{:?}", data_string, expected);
assert_eq!(data_string, expected);
}
}