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("s")]
Strikeout,
#[display("font")]
Font,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StartTag<'a> {
tag: Tag,
attributes: &'a str,
}
impl<'a> StartTag<'a> {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(tag: Tag, attributes: &'a str) -> Self {
Self { tag, attributes }
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn tag(&self) -> Tag {
self.tag
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn attributes(&self) -> &'a str {
self.attributes
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn attrs(&self) -> Attributes<'a> {
Attributes {
rest: self.attributes,
}
}
}
impl fmt::Display for StartTag<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<{}", self.tag)?;
if !self.attributes.is_empty() {
write!(f, " {}", self.attributes)?;
}
f.write_str(">")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Attribute<'a> {
name: &'a str,
value: Option<&'a str>,
}
impl<'a> Attribute<'a> {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn name(&self) -> &'a str {
self.name
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn value(&self) -> Option<&'a str> {
self.value
}
pub fn is_known(&self) -> bool {
["color", "size", "face"]
.iter()
.any(|known| self.name.eq_ignore_ascii_case(known))
}
}
impl fmt::Display for Attribute<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name)?;
match self.value {
None => Ok(()),
Some(value) if value.as_bytes().iter().any(u8::is_ascii_whitespace) => {
let quote = if value.contains('"') { '\'' } else { '"' };
write!(f, "={quote}{value}{quote}")
}
Some(value) => write!(f, "={value}"),
}
}
}
#[derive(Debug, Clone)]
pub struct Attributes<'a> {
rest: &'a str,
}
impl<'a> Iterator for Attributes<'a> {
type Item = Attribute<'a>;
fn next(&mut self) -> Option<Self::Item> {
let rest = self.rest.trim_start_matches([' ', '\t']);
if rest.is_empty() {
self.rest = rest;
return None;
}
let name_len = rest
.as_bytes()
.iter()
.take_while(|&&b| !matches!(b, b'=' | b' ' | b'\t'))
.count();
let name = &rest[..name_len];
let after = rest[name_len..].trim_start_matches([' ', '\t']);
let Some(after) = after.strip_prefix('=') else {
self.rest = after;
return Some(Attribute { name, value: None });
};
let after = after.trim_start_matches([' ', '\t']);
let (value, rest) = match after.as_bytes().first() {
Some("e @ (b'"' | b'\'')) => match after[1..].find(quote as char) {
Some(end) => (&after[1..1 + end], &after[end + 2..]),
None => (&after[1..], ""),
},
_ => {
let len = after
.as_bytes()
.iter()
.take_while(|&&b| !matches!(b, b' ' | b'\t'))
.count();
(&after[..len], &after[len..])
}
};
self.rest = rest;
Some(Attribute {
name,
value: Some(value),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct InlineCode<'a> {
raw: &'a str,
}
impl<'a> InlineCode<'a> {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(raw: &'a str) -> Self {
Self { raw }
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn as_str(&self) -> &'a str {
self.raw
}
}
impl fmt::Display for InlineCode<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{{{}}}", self.raw)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TextToken<'a> {
Text(&'a str),
StartTag(StartTag<'a>),
EndTag(Tag),
LineBreak,
InlineCode(InlineCode<'a>),
}
#[derive(Debug, Clone, Logos)]
enum RawTextToken<'a> {
#[regex(r"<[bB]>|<[bB][ \t][^<>]*>")]
StartBold(&'a str),
#[regex(r"<[iI]>|<[iI][ \t][^<>]*>")]
StartItalic(&'a str),
#[regex(r"<[uU]>|<[uU][ \t][^<>]*>")]
StartUnderline(&'a str),
#[regex(r"<[sS]>|<[sS][ \t][^<>]*>")]
StartStrikeout(&'a str),
#[regex(r"<[fF][oO][nN][tT]>|<[fF][oO][nN][tT][ \t][^<>]*>")]
StartFont(&'a str),
#[regex(r"</[bB]>|</[bB][ \t][^<>]*>")]
EndBold,
#[regex(r"</[iI]>|</[iI][ \t][^<>]*>")]
EndItalic,
#[regex(r"</[uU]>|</[uU][ \t][^<>]*>")]
EndUnderline,
#[regex(r"</[sS]>|</[sS][ \t][^<>]*>")]
EndStrikeout,
#[regex(r"</[fF][oO][nN][tT]>|</[fF][oO][nN][tT][ \t][^<>]*>")]
EndFont,
#[regex(r"</?[bB][rR]/?>|</?[bB][rR][ \t][^<>]*>")]
LineBreak,
#[regex(r"\{\\[^{}]*\}")]
SsaCode(&'a str),
#[regex(r"\{[CcFfoPSsYy]:[^{}]*\}")]
MicroDvdCode(&'a str),
#[regex(r"[^<{]+")]
Text(&'a str),
#[token("<")]
LiteralLt(&'a str),
#[token("{")]
LiteralBrace(&'a str),
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn start_tag<'a>(tag: Tag, slice: &'a str, name_len: usize) -> StartTag<'a> {
let attributes = slice[1 + name_len..slice.len() - 1].trim_start_matches([' ', '\t']);
StartTag { tag, attributes }
}
#[derive(Clone)]
pub struct TextParser<'a> {
lexer: logos::Lexer<'a, RawTextToken<'a>>,
}
impl<'a> TextParser<'a> {
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn new(input: &'a str) -> Self {
Self {
lexer: RawTextToken::lexer(input),
}
}
}
impl<'a> Iterator for TextParser<'a> {
type Item = TextToken<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
return Some(match self.lexer.next()? {
Ok(RawTextToken::StartBold(s)) => TextToken::StartTag(start_tag(Tag::Bold, s, 1)),
Ok(RawTextToken::StartItalic(s)) => TextToken::StartTag(start_tag(Tag::Italic, s, 1)),
Ok(RawTextToken::StartUnderline(s)) => TextToken::StartTag(start_tag(Tag::Underline, s, 1)),
Ok(RawTextToken::StartStrikeout(s)) => TextToken::StartTag(start_tag(Tag::Strikeout, s, 1)),
Ok(RawTextToken::StartFont(s)) => TextToken::StartTag(start_tag(Tag::Font, s, 4)),
Ok(RawTextToken::EndBold) => TextToken::EndTag(Tag::Bold),
Ok(RawTextToken::EndItalic) => TextToken::EndTag(Tag::Italic),
Ok(RawTextToken::EndUnderline) => TextToken::EndTag(Tag::Underline),
Ok(RawTextToken::EndStrikeout) => TextToken::EndTag(Tag::Strikeout),
Ok(RawTextToken::EndFont) => TextToken::EndTag(Tag::Font),
Ok(RawTextToken::LineBreak) => TextToken::LineBreak,
Ok(RawTextToken::SsaCode(s) | RawTextToken::MicroDvdCode(s)) => {
TextToken::InlineCode(InlineCode::new(&s[1..s.len() - 1]))
}
Ok(RawTextToken::Text(s) | RawTextToken::LiteralLt(s) | RawTextToken::LiteralBrace(s)) => {
TextToken::Text(s)
}
Err(()) => continue,
});
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Segment<'a> {
Text(&'a str),
LineBreak,
}
#[derive(Clone)]
pub struct Segments<'a> {
parser: TextParser<'a>,
}
impl<'a> Iterator for Segments<'a> {
type Item = Segment<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
return Some(match self.parser.next()? {
TextToken::Text(run) => Segment::Text(run),
TextToken::LineBreak => Segment::LineBreak,
TextToken::StartTag(_) | TextToken::EndTag(_) | TextToken::InlineCode(_) => continue,
});
}
}
}
pub struct PlainText<'a> {
raw: &'a str,
requires_normalization: bool,
#[cfg(any(feature = "alloc", feature = "std"))]
normalized: core::cell::OnceCell<std::string::String>,
}
impl<'a> PlainText<'a> {
pub fn new(raw: &'a str) -> Self {
let bytes = raw.as_bytes();
#[cfg(all(feature = "memchr", not(miri)))]
let dirty = memchr::memchr2(b'<', b'{', bytes).is_some();
#[cfg(not(all(feature = "memchr", not(miri))))]
let dirty = bytes.iter().any(|&b| b == b'<' || b == b'{');
if dirty {
Self::needs_normalization(raw)
} else {
Self::borrowed(raw)
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn borrowed(raw: &'a str) -> Self {
Self {
raw,
requires_normalization: false,
#[cfg(any(feature = "alloc", feature = "std"))]
normalized: core::cell::OnceCell::new(),
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn needs_normalization(raw: &'a str) -> Self {
Self {
raw,
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
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn segments(&self) -> Segments<'a> {
Segments {
parser: TextParser::new(self.raw),
}
}
pub fn normalize(&self) -> &str {
if !self.requires_normalization {
return self.raw;
}
#[cfg(any(feature = "alloc", feature = "std"))]
{
self.normalized.get_or_init(|| self.clean())
}
#[cfg(not(any(feature = "alloc", feature = "std")))]
{
self.raw
}
}
#[cfg(any(feature = "alloc", feature = "std"))]
fn clean(&self) -> std::string::String {
let mut out = std::string::String::with_capacity(self.raw.len());
for segment in self.segments() {
match segment {
Segment::Text(run) => out.push_str(run),
Segment::LineBreak => out.push('\n'),
}
}
out
}
}
impl Clone for PlainText<'_> {
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 PlainText<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PlainText")
.field("raw", &self.raw)
.field("requires_normalization", &self.requires_normalization)
.finish()
}
}
impl PartialEq for PlainText<'_> {
fn eq(&self, other: &Self) -> bool {
self.raw == other.raw && self.requires_normalization == other.requires_normalization
}
}
impl Eq for PlainText<'_> {}
impl fmt::Display for PlainText<'_> {
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")))]
{
for segment in self.segments() {
match segment {
Segment::Text(run) => f.write_str(run)?,
Segment::LineBreak => f.write_str("\n")?,
}
}
Ok(())
}
}
}