use std::iter::FusedIterator;
use crate::{TextElement, TtiBlock, text_field};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Subtitle<'a> {
blocks: &'a [TtiBlock],
}
impl<'a> Subtitle<'a> {
pub fn blocks(&self) -> &'a [TtiBlock] {
self.blocks
}
pub fn subtitle_number(&self) -> u16 {
self.blocks[0].sn
}
pub fn header_block(&self) -> &'a TtiBlock {
self.text_blocks().next().unwrap_or(&self.blocks[0])
}
pub fn is_comment(&self) -> bool {
self.header_block().is_comment()
}
pub fn text_blocks(&self) -> impl Iterator<Item = &'a TtiBlock> + use<'a> {
self.blocks.iter().filter(|block| block.is_text_block())
}
pub fn user_data_blocks(&self) -> impl Iterator<Item = &'a TtiBlock> + use<'a> {
self.blocks.iter().filter(|block| block.is_user_data())
}
pub fn text(&self) -> String {
text_field::decode(&self.concatenated_text_field(), self.blocks[0].cct)
}
pub fn text_elements(&self) -> Vec<TextElement> {
text_field::decode_elements(&self.concatenated_text_field(), self.blocks[0].cct)
}
fn concatenated_text_field(&self) -> Vec<u8> {
self.text_blocks()
.flat_map(|block| block.text_field_content())
.copied()
.collect()
}
}
#[derive(Debug, Clone)]
pub struct Subtitles<'a> {
remaining: &'a [TtiBlock],
}
impl<'a> Subtitles<'a> {
pub(crate) fn new(blocks: &'a [TtiBlock]) -> Self {
Self { remaining: blocks }
}
}
impl<'a> Iterator for Subtitles<'a> {
type Item = Subtitle<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining.is_empty() {
return None;
}
let (blocks, remaining) = self.remaining.split_at(subtitle_len(self.remaining));
self.remaining = remaining;
Some(Subtitle { blocks })
}
}
impl FusedIterator for Subtitles<'_> {}
fn subtitle_len(blocks: &[TtiBlock]) -> usize {
let Some(first) = blocks.first() else {
return 0;
};
let mut ended = first.is_last_block();
let mut len = 1;
for block in &blocks[1..] {
if block.sn != first.sn || (ended && block.is_text_block()) {
break;
}
ended |= block.is_last_block();
len += 1;
}
len
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{CharacterCodeTable, CumulativeStatus, Stl, Time};
const TF_LENGTH: usize = 112;
fn time(seconds: u8) -> Time {
Time {
hours: 0,
minutes: 0,
seconds,
frames: 0,
}
}
fn block(sn: u16, ebn: u8, text: &[u8]) -> TtiBlock {
assert!(text.len() <= TF_LENGTH);
let mut tf = text.to_vec();
tf.resize(TF_LENGTH, 0x8F);
TtiBlock {
sgn: 0,
sn,
ebn,
cs: CumulativeStatus::NotPartOfASet,
tci: time(1),
tco: time(2),
vp: 20,
jc: 2,
cf: 0,
tf,
cct: CharacterCodeTable::Latin,
}
}
fn stl(ttis: Vec<TtiBlock>) -> Stl {
Stl {
gsi: crate::GsiBlock::new(),
ttis,
}
}
fn block_counts(stl: &Stl) -> Vec<usize> {
stl.subtitles().map(|s| s.blocks().len()).collect()
}
#[test]
fn no_blocks_gives_no_subtitles() {
assert_eq!(0, stl(vec![]).subtitles().count());
}
#[test]
fn single_block_subtitles() {
let stl = stl(vec![
block(1, 0xFF, b"one"),
block(2, 0xFF, b"two"),
block(3, 0xFF, b"three"),
]);
let texts: Vec<String> = stl.subtitles().map(|s| s.text()).collect();
assert_eq!(vec!["one", "two", "three"], texts);
let numbers: Vec<u16> = stl.subtitles().map(|s| s.subtitle_number()).collect();
assert_eq!(vec![1, 2, 3], numbers);
}
#[test]
fn extension_blocks_are_merged() {
let mut first = b"Line one".to_vec();
first.push(0x8A);
first.resize(TF_LENGTH - 3, b'x');
first.extend(b"Bee");
let stl = stl(vec![
block(1, 0x00, &first),
block(1, 0x01, b"kem"),
block(1, 0xFF, b"olen."),
block(2, 0xFF, b"next"),
]);
assert_eq!(vec![3, 1], block_counts(&stl));
let text = stl.subtitles().next().unwrap().text();
assert!(text.starts_with("Line one\r\nxxx"), "{text:?}");
assert!(text.ends_with("xxxBeekemolen."), "{text:?}");
}
#[test]
fn character_split_across_blocks_is_decoded() {
let stl = stl(vec![block(1, 0x00, b"B\xC8"), block(1, 0xFF, b"ar")]);
assert_eq!("Bär", stl.subtitles().next().unwrap().text());
}
#[test]
fn user_data_is_not_part_of_the_text() {
let stl = stl(vec![
block(1, 0xFE, b"user data first"),
block(1, 0x00, b"Hello "),
block(1, 0xFE, b"user data between"),
block(1, 0xFF, b"world"),
block(1, 0xFE, b"user data last"),
block(2, 0xFF, b"next"),
]);
assert_eq!(vec![5, 1], block_counts(&stl));
let subtitle = stl.subtitles().next().unwrap();
assert_eq!("Hello world", subtitle.text());
let user_data: Vec<String> = subtitle
.user_data_blocks()
.map(|block| block.text())
.collect();
assert_eq!(
vec!["user data first", "user data between", "user data last"],
user_data
);
assert_eq!(2, subtitle.text_blocks().count());
}
#[test]
fn reserved_blocks_are_not_part_of_the_text() {
let stl = stl(vec![
block(1, 0x00, b"Hello "),
block(1, 0xF0, b"reserved"),
block(1, 0xFD, b"reserved"),
block(1, 0xFF, b"world"),
]);
let subtitle = stl.subtitles().next().unwrap();
assert_eq!(4, subtitle.blocks().len());
assert_eq!("Hello world", subtitle.text());
}
#[test]
fn header_block_is_first_text_block() {
let mut text_block = block(1, 0xFF, b"text");
text_block.tci = time(10);
let stl = stl(vec![block(1, 0xFE, b"user data"), text_block]);
let subtitle = stl.subtitles().next().unwrap();
assert_eq!(time(10), subtitle.header_block().tci);
}
#[test]
fn header_block_without_text_blocks_is_first_block() {
let stl = stl(vec![block(1, 0xFE, b"user data")]);
let subtitle = stl.subtitles().next().unwrap();
assert!(subtitle.header_block().is_user_data());
assert_eq!("", subtitle.text());
}
#[test]
fn same_subtitle_number_after_last_block_starts_new_subtitle() {
let stl = stl(vec![
block(0, 0xFF, b"one"),
block(0, 0xFF, b"two"),
block(0, 0x00, b"thr"),
block(0, 0xFF, b"ee"),
]);
let texts: Vec<String> = stl.subtitles().map(|s| s.text()).collect();
assert_eq!(vec!["one", "two", "three"], texts);
}
#[test]
fn subtitle_without_last_block_ends_at_next_subtitle_number() {
let stl = stl(vec![block(1, 0x00, b"one"), block(2, 0xFF, b"two")]);
assert_eq!(vec![1, 1], block_counts(&stl));
}
#[test]
fn comment_flag() {
let mut comment = block(1, 0x00, b"comment ");
comment.cf = 1;
let stl = stl(vec![
comment,
block(1, 0xFF, b"text"),
block(2, 0xFF, b"sub"),
]);
let comments: Vec<bool> = stl.subtitles().map(|s| s.is_comment()).collect();
assert_eq!(vec![true, false], comments);
}
#[test]
fn block_text_filling_the_whole_field_is_decoded() {
let text = [b'a'; TF_LENGTH];
assert_eq!("a".repeat(TF_LENGTH), block(1, 0xFF, &text).text());
}
#[test]
fn block_text_stops_at_unused_space() {
let tti = block(1, 0xFF, b"one\x8Atwo\x8Fignored");
assert_eq!("one\r\ntwo", tti.text());
}
#[test]
fn parsed_file_has_one_subtitle_per_block() {
let stl = crate::parse_stl_from_file("stls/test.stl").expect("Parse stls/test.stl");
assert_eq!(stl.ttis.len(), stl.subtitles().count());
for (subtitle, tti) in stl.subtitles().zip(&stl.ttis) {
assert_eq!(tti.text(), subtitle.text());
}
}
#[test]
fn text_elements_continue_across_blocks() {
use crate::OpenControl;
let stl = stl(vec![block(1, 0x00, b"\x80Hel"), block(1, 0xFF, b"lo\x81")]);
assert_eq!(
vec![
TextElement::Open(OpenControl::ItalicsOn),
TextElement::Text("Hello".to_string()),
TextElement::Open(OpenControl::ItalicsOff),
],
stl.subtitles().next().unwrap().text_elements()
);
}
}