use crate::{error::*, utils::Lines};
pub use types::{
Block, Event, EventField, EventFormat, EventKind, Fields, Format, Hour, Property, Section,
StyleRow, Timestamp,
};
mod types;
pub mod text;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ParseAssError {
#[error(transparent)]
ParseHour(#[from] ParseHourError),
#[error(transparent)]
ParseMinute(#[from] ParseMinuteError),
#[error(transparent)]
ParseSecond(#[from] ParseSecondError),
#[error(transparent)]
ParseCentisecond(#[from] ParseCentisecondError),
#[error("invalid timestamp: {0}")]
InvalidTimestamp(TimestampError),
#[error("unclosed section header, missing ']'")]
UnclosedSection,
#[error("event row before the '[Events]' section declared a 'Format:' line")]
MissingFormat,
#[error("event row declares {expected} fields, but only {found} were found")]
TooFewFields {
expected: usize,
found: usize,
},
#[error("invalid value for the '{0}' field")]
InvalidField(EventField),
#[error("unexpected line")]
UnexpectedLine,
#[error("unexpected token: {0}")]
Unknown(&'static str),
}
impl Default for ParseAssError {
fn default() -> Self {
Self::Unknown("unknown lexer error")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case", default))]
pub struct Options {
allow_missing_format: bool,
allow_short_event: bool,
allow_malformed_fields: bool,
ignore_unknown_lines: bool,
}
impl Options {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn strict() -> Self {
Self {
allow_missing_format: false,
allow_short_event: false,
allow_malformed_fields: false,
ignore_unknown_lines: false,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn lossy() -> Self {
Self {
allow_missing_format: true,
allow_short_event: true,
allow_malformed_fields: true,
ignore_unknown_lines: true,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn allow_missing_format(&self) -> bool {
self.allow_missing_format
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn with_allow_missing_format(mut self, value: bool) -> Self {
self.allow_missing_format = value;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_allow_missing_format(&mut self, value: bool) -> &mut Self {
self.allow_missing_format = value;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn allow_short_event(&self) -> bool {
self.allow_short_event
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn with_allow_short_event(mut self, value: bool) -> Self {
self.allow_short_event = value;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_allow_short_event(&mut self, value: bool) -> &mut Self {
self.allow_short_event = value;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn allow_malformed_fields(&self) -> bool {
self.allow_malformed_fields
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn with_allow_malformed_fields(mut self, value: bool) -> Self {
self.allow_malformed_fields = value;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_allow_malformed_fields(&mut self, value: bool) -> &mut Self {
self.allow_malformed_fields = value;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn ignore_unknown_lines(&self) -> bool {
self.ignore_unknown_lines
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn with_ignore_unknown_lines(mut self, value: bool) -> Self {
self.ignore_unknown_lines = value;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_ignore_unknown_lines(&mut self, value: bool) -> &mut Self {
self.ignore_unknown_lines = value;
self
}
}
impl Default for Options {
fn default() -> Self {
Self::strict()
}
}
pub struct Parser<'a> {
lines: Lines<'a>,
options: Options,
section: Option<Section<'a>>,
event_format: Option<EventFormat>,
first_line: bool,
done: bool,
}
impl<'a> Parser<'a> {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn strict(input: &'a str) -> Self {
Self::with_options(input, Options::strict())
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn lossy(input: &'a str) -> Self {
Self::with_options(input, Options::lossy())
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn with_options(input: &'a str, options: Options) -> Self {
Self {
lines: Lines::new(input),
options,
section: None,
event_format: None,
first_line: true,
done: false,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn section(&self) -> Option<Section<'a>> {
self.section
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn event_format(&self) -> Option<EventFormat> {
self.event_format
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn resolve_event_format(&self) -> Result<EventFormat, ParseAssError> {
match self.event_format {
Some(format) => Ok(format),
None if self.options.allow_missing_format => Ok(EventFormat::ass()),
None => Err(ParseAssError::MissingFormat),
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn fail(&mut self, error: ParseAssError) -> Option<Result<Block<'a>, ParseAssError>> {
self.done = true;
Some(Err(error))
}
}
impl<'a> Iterator for Parser<'a> {
type Item = Result<Block<'a>, ParseAssError>;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.done {
return None;
}
let mut raw_line = self.lines.next()?;
if self.first_line {
self.first_line = false;
raw_line = raw_line.trim_start_matches('\u{feff}');
}
let line = raw_line.trim_start();
if line.trim_end().is_empty() {
continue;
}
if let Some(rest) = line.strip_prefix('[') {
let Some(name) = rest.trim_end().strip_suffix(']') else {
if self.options.ignore_unknown_lines {
continue;
}
return self.fail(ParseAssError::UnclosedSection);
};
let section = Section::new(name);
self.section = Some(section);
if section.is_events() {
self.event_format = None;
}
return Some(Ok(Block::Section(section)));
}
if let Some(section) = self.section
&& section.is_resource()
{
let header = match section {
Section::Fonts => "fontname:",
_ => "filename:",
};
if !line.starts_with(header) {
return Some(Ok(Block::Data(raw_line)));
}
}
if let Some(rest) = line.strip_prefix(';') {
return Some(Ok(Block::Comment(rest)));
}
let Some((key, value)) = line.split_once(':') else {
if self.options.ignore_unknown_lines {
continue;
}
return self.fail(ParseAssError::UnexpectedLine);
};
let key = key.trim_end();
let value = value.trim_start_matches([' ', '\t']);
if key.is_empty() {
if self.options.ignore_unknown_lines {
continue;
}
return self.fail(ParseAssError::UnexpectedLine);
}
if key.eq_ignore_ascii_case("Format") {
let format = Format::new(value);
if self.section == Some(Section::Events) {
self.event_format = Some(format.event_format());
}
return Some(Ok(Block::Format(format)));
}
if key.eq_ignore_ascii_case("Style")
&& self.section.is_some_and(|section| section.is_styles())
{
return Some(Ok(Block::Style(StyleRow::new(value))));
}
if self.section == Some(Section::Events)
&& let Some(kind) = EventKind::new(key)
{
let format = match self.resolve_event_format() {
Ok(format) => format,
Err(error) => return self.fail(error),
};
return match Event::parse_fields_with(kind, value, &format, &self.options) {
Ok(event) => Some(Ok(Block::Event(event))),
Err(error) => self.fail(error),
};
}
return Some(Ok(Block::Property(Property::new(key, value))));
}
}
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub struct Writer<W> {
inner: W,
format: EventFormat,
default_format: EventFormat,
in_events: bool,
has_written: bool,
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
const _: () = {
use std::io::{self, Write};
impl<W: Write> Writer<W> {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(inner: W) -> Self {
Self {
inner,
format: EventFormat::ass(),
default_format: EventFormat::ass(),
in_events: false,
has_written: false,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn with_event_format(inner: W, format: EventFormat) -> Self {
Self {
inner,
format,
default_format: format,
in_events: false,
has_written: false,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn event_format(&self) -> EventFormat {
self.format
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_event_format(&mut self, format: EventFormat) -> &mut Self {
self.format = format;
self
}
pub fn write(&mut self, block: &Block<'_>) -> io::Result<()> {
let result = self.write_block(block);
self.has_written = true;
result
}
fn write_block(&mut self, block: &Block<'_>) -> io::Result<()> {
match block {
Block::Section(section) => {
if self.has_written {
self.inner.write_all(b"\n")?;
}
self.in_events = section.is_events();
if self.in_events {
self.format = self.default_format;
}
writeln!(self.inner, "[{}]", section.as_str())
}
Block::Comment(text) => writeln!(self.inner, ";{text}"),
Block::Format(format) => {
if self.in_events {
self.format = format.event_format();
}
writeln!(self.inner, "Format: {}", format.as_str())
}
Block::Style(row) => writeln!(self.inner, "Style: {}", row.as_str()),
Block::Data(payload) => writeln!(self.inner, "{payload}"),
Block::Event(event) => self.write_event(event),
Block::Property(property) => {
writeln!(self.inner, "{}: {}", property.key(), property.value())
}
}
}
pub fn write_all<'b, 'c, I>(&mut self, blocks: I) -> io::Result<()>
where
I: IntoIterator<Item = &'b Block<'c>>,
'c: 'b,
{
for block in blocks {
self.write(block)?;
}
Ok(())
}
pub fn write_event(&mut self, event: &Event<'_>) -> io::Result<()> {
self.has_written = true;
write!(self.inner, "{}: ", event.kind().as_str())?;
for index in 0..self.format.len() {
if index > 0 {
self.inner.write_all(b",")?;
}
let Some(field) = self.format.field_at(index) else {
if let Some(raw) = event.field(index) {
self.inner.write_all(raw.as_bytes())?;
}
continue;
};
match field {
EventField::ReadOrder => write_opt(&mut self.inner, event.read_order())?,
EventField::Marked => write_opt(&mut self.inner, event.marked())?,
EventField::Layer => write_opt(&mut self.inner, event.layer())?,
EventField::Start => write_timestamp(&mut self.inner, event.start())?,
EventField::End => write_timestamp(&mut self.inner, event.end())?,
EventField::Style => write_opt(&mut self.inner, event.style())?,
EventField::Name => write_opt(&mut self.inner, event.name())?,
EventField::MarginL => write_opt(&mut self.inner, event.margin_l())?,
EventField::MarginR => write_opt(&mut self.inner, event.margin_r())?,
EventField::MarginV => write_opt(&mut self.inner, event.margin_v())?,
EventField::Effect => write_opt(&mut self.inner, event.effect())?,
EventField::Text => self.inner.write_all(event.text().as_bytes())?,
}
}
self.inner.write_all(b"\n")
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn into_inner(self) -> W {
self.inner
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn write_opt<W: Write, T: core::fmt::Display>(w: &mut W, value: Option<T>) -> io::Result<()> {
match value {
Some(value) => write!(w, "{value}"),
None => Ok(()),
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn write_timestamp<W: Write>(w: &mut W, value: Option<Timestamp>) -> io::Result<()> {
match value {
Some(value) => w.write_all(value.encode().as_str().as_bytes()),
None => Ok(()),
}
}
};