use derive_more::{Display, IsVariant};
use logos::Logos;
use core::fmt;
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, IsVariant)]
pub enum Tag {
#[display("b")]
Bold,
#[display("i")]
Italic,
#[display("u")]
Underline,
#[display("c")]
Class,
#[display("ruby")]
Ruby,
#[display("rt")]
RubyText,
#[display("v")]
Voice,
#[display("lang")]
Lang,
}
#[derive(Debug, Logos)]
enum RawCueToken<'a> {
#[regex(r"[^<]+")]
Text(&'a str),
#[token("</b>")]
EndBold,
#[token("</i>")]
EndItalic,
#[token("</u>")]
EndUnderline,
#[token("</c>")]
EndClass,
#[token("</ruby>")]
EndRuby,
#[token("</rt>")]
EndRubyText,
#[token("</v>")]
EndVoice,
#[token("</lang>")]
EndLang,
#[regex(r"<b[. \t][^>]*>|<b>")]
StartBold(&'a str),
#[regex(r"<i[. \t][^>]*>|<i>")]
StartItalic(&'a str),
#[regex(r"<u[. \t][^>]*>|<u>")]
StartUnderline(&'a str),
#[regex(r"<c[. \t][^>]*>|<c>")]
StartClass(&'a str),
#[regex(r"<ruby[. \t][^>]*>|<ruby>")]
StartRuby(&'a str),
#[regex(r"<rt[. \t][^>]*>|<rt>")]
StartRubyText(&'a str),
#[regex(r"<v[. \t][^>]*>|<v>")]
StartVoice(&'a str),
#[regex(r"<lang[. \t][^>]*>|<lang>")]
StartLang(&'a str),
#[regex(r"<[0-9:]+\.[0-9]{3}>")]
Timestamp(&'a str),
#[regex(r"<[^>]*>")]
UnknownTag,
#[regex(r"<[^>]*")]
UnterminatedTag,
}
pub struct CueStr<'a> {
raw: &'a str,
requires_normalization: bool,
#[cfg(any(feature = "alloc", feature = "std"))]
normalized: core::cell::OnceCell<std::string::String>,
}
impl<'a> CueStr<'a> {
pub const fn borrowed(s: &'a str) -> Self {
Self {
raw: s,
requires_normalization: false,
#[cfg(any(feature = "alloc", feature = "std"))]
normalized: core::cell::OnceCell::new(),
}
}
pub const fn needs_normalization(s: &'a str) -> Self {
Self {
raw: s,
requires_normalization: true,
#[cfg(any(feature = "alloc", feature = "std"))]
normalized: core::cell::OnceCell::new(),
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn as_raw(&self) -> &'a str {
self.raw
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn requires_normalization(&self) -> bool {
self.requires_normalization
}
pub fn normalize(&self) -> &str {
if !self.requires_normalization {
return self.raw;
}
#[cfg(any(feature = "alloc", feature = "std"))]
{
self.normalized.get_or_init(|| self.decode_char_refs())
}
#[cfg(not(any(feature = "alloc", feature = "std")))]
{
self.raw
}
}
#[cfg(any(feature = "alloc", feature = "std"))]
fn decode_char_refs(&self) -> std::string::String {
let input = self.as_raw();
let bytes = input.as_bytes();
let len = bytes.len();
#[cfg(all(feature = "memchr", not(miri)))]
let has_special = memchr::memchr2(b'&', 0, bytes).is_some();
#[cfg(not(all(feature = "memchr", not(miri))))]
let has_special = bytes.iter().any(|&b| b == b'&' || b == 0);
if !has_special {
return std::string::String::from(input);
}
let mut out = std::string::String::with_capacity(len);
let mut i = 0;
while i < len {
if bytes[i] == 0 {
out.push('\u{FFFD}');
i += 1;
} else if bytes[i] == b'&' {
i += 1; if i >= len {
out.push('&');
continue;
}
if bytes[i] == b'#' {
i += 1;
if i >= len {
out.push_str("&#");
continue;
}
let hex = bytes[i] == b'x' || bytes[i] == b'X';
if hex {
i += 1;
}
let start = i;
if hex {
while i < len && bytes[i].is_ascii_hexdigit() {
i += 1;
}
} else {
while i < len && bytes[i].is_ascii_digit() {
i += 1;
}
}
if start == i {
out.push_str(if hex { "&#x" } else { "&#" });
continue;
}
let digits = &input[start..i];
let code_point = if hex {
u32::from_str_radix(digits, 16).unwrap_or(0xFFFD)
} else {
digits.parse::<u32>().unwrap_or(0xFFFD)
};
if i < len && bytes[i] == b';' {
i += 1;
}
if code_point == 0 {
out.push('\u{FFFD}');
} else if let Some(c) = char::from_u32(code_point) {
out.push(c);
} else {
out.push('\u{FFFD}');
}
} else if bytes[i].is_ascii_alphanumeric() {
let ref_start = i;
while i < len && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b';') {
i += 1;
if bytes[i - 1] == b';' {
break;
}
}
let candidate = &input[ref_start..i];
match Self::find_longest_entity_match(candidate) {
Some((matched_len, decoded)) => {
out.push_str(decoded);
i = ref_start + matched_len;
}
None => {
out.push('&');
i = ref_start; }
}
} else {
out.push('&');
}
} else {
let start = i;
while i < len && bytes[i] != b'&' && bytes[i] != 0 {
i += 1;
}
out.push_str(&input[start..i]);
}
}
out
}
#[cfg(any(feature = "alloc", feature = "std"))]
fn find_longest_entity_match(candidate: &str) -> Option<(usize, &'static str)> {
use super::html5_entities::HTML5_ENTITIES;
const MAX_ENTITY_LEN: usize = 32;
let mut best: Option<(usize, &'static str)> = None;
let limit = candidate.len().min(MAX_ENTITY_LEN);
for end in 1..=limit {
let prefix = &candidate[..end];
if let Some(s) = HTML5_ENTITIES.get(prefix) {
best = Some((end, s));
if prefix.ends_with(';') {
break;
}
}
}
best
}
}
impl Clone for CueStr<'_> {
fn clone(&self) -> Self {
Self {
raw: self.raw,
requires_normalization: self.requires_normalization,
#[cfg(any(feature = "alloc", feature = "std"))]
normalized: self.normalized.clone(),
}
}
}
impl fmt::Debug for CueStr<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CueStr")
.field("raw", &self.raw)
.field("requires_normalization", &self.requires_normalization)
.finish()
}
}
impl PartialEq for CueStr<'_> {
fn eq(&self, other: &Self) -> bool {
self.raw == other.raw && self.requires_normalization == other.requires_normalization
}
}
impl Eq for CueStr<'_> {}
impl fmt::Display for CueStr<'_> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
#[cfg(any(feature = "alloc", feature = "std"))]
{
f.write_str(self.normalize())
}
#[cfg(not(any(feature = "alloc", feature = "std")))]
{
f.write_str(self.raw)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CueToken<'a> {
Text(CueStr<'a>),
StartTag {
tag: Tag,
classes: &'a str,
annotation: Option<&'a str>,
},
EndTag(Tag),
Timestamp(crate::vtt::Timestamp),
}
pub struct CueParser<'a> {
lexer: logos::Lexer<'a, RawCueToken<'a>>,
}
impl<'a> CueParser<'a> {
pub fn new(input: &'a str) -> Self {
Self {
lexer: RawCueToken::lexer(input),
}
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn parse_tag_attrs(after_name: &str) -> (&str, Option<&str>) {
if after_name.is_empty() {
return ("", None);
}
let (tag_rest, annotation) = match after_name.find([' ', '\t']) {
Some(idx) => {
let ann = after_name[idx + 1..].trim();
(
&after_name[..idx],
if ann.is_empty() { None } else { Some(ann) },
)
}
None => (after_name, None),
};
let classes = tag_rest.strip_prefix('.').unwrap_or("");
(classes, annotation)
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn make_start_tag<'a>(tag: Tag, slice: &'a str, name_len: usize) -> CueToken<'a> {
let inner = &slice[1 + name_len..slice.len() - 1];
let (classes, annotation) = parse_tag_attrs(inner);
CueToken::StartTag {
tag,
classes,
annotation,
}
}
impl<'a> Iterator for CueParser<'a> {
type Item = CueToken<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let token = self.lexer.next()?;
match token {
Ok(RawCueToken::Text(text)) => {
let needs_norm = text.as_bytes().iter().any(|&b| b == b'&' || b == 0);
return Some(CueToken::Text(if needs_norm {
CueStr::needs_normalization(text)
} else {
CueStr::borrowed(text)
}));
}
Ok(RawCueToken::EndBold) => return Some(CueToken::EndTag(Tag::Bold)),
Ok(RawCueToken::EndItalic) => return Some(CueToken::EndTag(Tag::Italic)),
Ok(RawCueToken::EndUnderline) => return Some(CueToken::EndTag(Tag::Underline)),
Ok(RawCueToken::EndClass) => return Some(CueToken::EndTag(Tag::Class)),
Ok(RawCueToken::EndRuby) => return Some(CueToken::EndTag(Tag::Ruby)),
Ok(RawCueToken::EndRubyText) => return Some(CueToken::EndTag(Tag::RubyText)),
Ok(RawCueToken::EndVoice) => return Some(CueToken::EndTag(Tag::Voice)),
Ok(RawCueToken::EndLang) => return Some(CueToken::EndTag(Tag::Lang)),
Ok(RawCueToken::StartBold(s)) => return Some(make_start_tag(Tag::Bold, s, 1)),
Ok(RawCueToken::StartItalic(s)) => return Some(make_start_tag(Tag::Italic, s, 1)),
Ok(RawCueToken::StartUnderline(s)) => {
return Some(make_start_tag(Tag::Underline, s, 1));
}
Ok(RawCueToken::StartClass(s)) => return Some(make_start_tag(Tag::Class, s, 1)),
Ok(RawCueToken::StartRuby(s)) => return Some(make_start_tag(Tag::Ruby, s, 4)),
Ok(RawCueToken::StartRubyText(s)) => {
return Some(make_start_tag(Tag::RubyText, s, 2));
}
Ok(RawCueToken::StartVoice(s)) => return Some(make_start_tag(Tag::Voice, s, 1)),
Ok(RawCueToken::StartLang(s)) => return Some(make_start_tag(Tag::Lang, s, 4)),
Ok(RawCueToken::Timestamp(s)) => {
let content = &s[1..s.len() - 1]; if let Ok(ts) = super::parse_timestamp(content) {
return Some(CueToken::Timestamp(ts));
}
}
Ok(RawCueToken::UnknownTag) | Err(()) => {}
Ok(RawCueToken::UnterminatedTag) => {
let s = self.lexer.slice();
if let Some(token) = try_parse_unterminated(s) {
return Some(token);
}
}
}
}
}
}
fn try_parse_unterminated<'a>(slice: &'a str) -> Option<CueToken<'a>> {
let inner = &slice[1..]; if inner.is_empty() {
return None;
}
if inner.as_bytes()[0].is_ascii_digit() {
if let Ok(ts) = super::parse_timestamp(inner) {
return Some(CueToken::Timestamp(ts));
}
return None;
}
let (tag, name_len) = match inner.as_bytes() {
[b'b', b'.' | b' ' | b'\t', ..] | [b'b'] => (Tag::Bold, 1),
[b'i', b'.' | b' ' | b'\t', ..] | [b'i'] => (Tag::Italic, 1),
[b'u', b'.' | b' ' | b'\t', ..] | [b'u'] => (Tag::Underline, 1),
[b'c', b'.' | b' ' | b'\t', ..] | [b'c'] => (Tag::Class, 1),
[b'v', b'.' | b' ' | b'\t', ..] | [b'v'] => (Tag::Voice, 1),
_ if inner.starts_with("ruby") => {
if inner.len() == 4 || matches!(inner.as_bytes()[4], b'.' | b' ' | b'\t') {
(Tag::Ruby, 4)
} else {
return None;
}
}
_ if inner.starts_with("rt") => {
if inner.len() == 2 || matches!(inner.as_bytes()[2], b'.' | b' ' | b'\t') {
(Tag::RubyText, 2)
} else {
return None;
}
}
_ if inner.starts_with("lang") => {
if inner.len() == 4 || matches!(inner.as_bytes()[4], b'.' | b' ' | b'\t') {
(Tag::Lang, 4)
} else {
return None;
}
}
_ => return None,
};
let after_name = &inner[name_len..];
let (classes, annotation) = parse_tag_attrs(after_name);
Some(CueToken::StartTag {
tag,
classes,
annotation,
})
}
#[cfg(any(feature = "alloc", feature = "std"))]
mod tree {
use derive_more::{TryUnwrap, Unwrap};
use super::*;
use crate::vtt::Timestamp;
use std::vec::Vec;
#[derive(Debug, Clone, PartialEq, Eq, IsVariant, Unwrap, TryUnwrap)]
#[unwrap(ref, ref_mut)]
#[try_unwrap(ref, ref_mut)]
pub enum Node<'a> {
Text(CueStr<'a>),
Timestamp(Timestamp),
Tag(TagNode<'a>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TagNode<'a> {
tag: Tag,
classes: &'a str,
annotation: Option<&'a str>,
children: Vec<Node<'a>>,
}
impl<'a> TagNode<'a> {
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn new(tag: Tag) -> Self {
Self {
tag,
classes: "",
annotation: None,
children: Vec::new(),
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn tag(&self) -> Tag {
self.tag
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn with_tag(mut self, tag: Tag) -> Self {
self.tag = tag;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn set_tag(&mut self, tag: Tag) -> &mut Self {
self.tag = tag;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn classes(&self) -> &'a str {
self.classes
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn with_classes(mut self, classes: &'a str) -> Self {
self.classes = classes;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn set_classes(&mut self, classes: &'a str) -> &mut Self {
self.classes = classes;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn annotation(&self) -> Option<&'a str> {
self.annotation
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn with_annotation(mut self, annotation: Option<&'a str>) -> Self {
self.annotation = annotation;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn set_annotation(&mut self, annotation: Option<&'a str>) -> &mut Self {
self.annotation = annotation;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn children(&self) -> &[Node<'a>] {
&self.children
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn children_mut(&mut self) -> &mut Vec<Node<'a>> {
&mut self.children
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn with_children(mut self, children: Vec<Node<'a>>) -> Self {
self.children = children;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn set_children(&mut self, children: Vec<Node<'a>>) -> &mut Self {
self.children = children;
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn into_children(self) -> Vec<Node<'a>> {
self.children
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CueText<'a> {
children: Vec<Node<'a>>,
}
impl<'a> CueText<'a> {
pub fn parse(input: &'a str) -> Self {
let tokens: Vec<_> = CueParser::new(input).collect();
let mut root_children = Vec::new();
let mut stack: Vec<TagNode<'a>> = Vec::new();
for token in tokens {
match token {
CueToken::Text(text) => {
let node = Node::Text(text);
if let Some(parent) = stack.last_mut() {
parent.children_mut().push(node);
} else {
root_children.push(node);
}
}
CueToken::Timestamp(ts) => {
let node = Node::Timestamp(ts);
if let Some(parent) = stack.last_mut() {
parent.children_mut().push(node);
} else {
root_children.push(node);
}
}
CueToken::StartTag {
tag,
classes,
annotation,
} => {
if tag == Tag::RubyText && !stack.iter().any(|n| n.tag() == Tag::Ruby) {
continue;
}
stack.push(
TagNode::new(tag)
.with_classes(classes)
.with_annotation(annotation),
);
}
CueToken::EndTag(tag) => {
if tag == Tag::RubyText && !stack.iter().any(|n| n.tag() == Tag::Ruby) {
continue;
}
while stack.last().is_some_and(|n| n.tag() == Tag::RubyText) {
let rt = stack.pop().unwrap();
let target = stack
.last_mut()
.map_or(&mut root_children, |p| p.children_mut());
target.push(Node::Tag(rt));
}
if stack.last().is_some_and(|n| n.tag() == tag) {
let node = stack.pop().unwrap();
let target = stack
.last_mut()
.map_or(&mut root_children, |p| p.children_mut());
target.push(Node::Tag(node));
}
}
}
}
while let Some(node) = stack.pop() {
let completed = Node::Tag(node);
if let Some(parent) = stack.last_mut() {
parent.children_mut().push(completed);
} else {
root_children.push(completed);
}
}
Self {
children: root_children,
}
}
pub fn children(&self) -> &[Node<'a>] {
&self.children
}
}
}
#[cfg(any(feature = "alloc", feature = "std"))]
pub use tree::*;