use logos::Logos;
use core::fmt;
static KNOWN_TAGS: &[&str] = &[
"alpha", "iclip", "xbord", "xshad", "ybord", "yshad", "blur", "bord", "clip", "fade", "fscx", "fscy", "move", "shad", "fad", "fax", "fay", "frx", "fry", "frz", "fsc", "fsp", "org", "pbo", "pos", "1a", "1c", "2a", "2c", "3a", "3c", "4a", "4c", "an", "be", "fe", "fn", "fr", "fs", "kf", "ko",
"kt", "K", "a", "b", "c", "i", "k", "p", "q", "r", "s", "t", "u",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TextToken<'a> {
Text(&'a str),
EscapedBrace(&'a str),
HardBreak,
SoftBreak,
HardSpace,
Override(Override<'a>),
Drawing(&'a str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Override<'a> {
raw: &'a str,
}
impl<'a> Override<'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
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn tags(&self) -> OverrideTags<'a> {
OverrideTags { rest: self.raw }
}
pub fn drawing_scale(&self) -> Option<u32> {
let mut scale = None;
for tag in self.tags() {
if tag.name() == "p" {
scale = Some(tag.args().trim().parse::<u32>().unwrap_or(0));
}
}
scale
}
}
impl fmt::Display for Override<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("{")?;
f.write_str(self.raw)?;
f.write_str("}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OverrideTag<'a> {
name: &'a str,
args: &'a str,
known: bool,
}
impl<'a> OverrideTag<'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 args(&self) -> &'a str {
self.args
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn is_known(&self) -> bool {
self.known
}
}
impl fmt::Display for OverrideTag<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("\\")?;
f.write_str(self.name)?;
f.write_str(self.args)
}
}
#[derive(Debug, Clone)]
pub struct OverrideTags<'a> {
rest: &'a str,
}
impl<'a> Iterator for OverrideTags<'a> {
type Item = OverrideTag<'a>;
fn next(&mut self) -> Option<Self::Item> {
let start = find_tag_start(self.rest)?;
let after = &self.rest[start + 1..];
let spaces = leading_spaces(after);
let (name, known) = match_tag_name(&after[spaces..]);
let args_start = start + 1 + spaces + name.len();
let args_len = find_tag_start(&self.rest[args_start..]).unwrap_or(self.rest.len() - args_start);
let args = &self.rest[args_start..args_start + args_len];
self.rest = &self.rest[args_start + args_len..];
Some(OverrideTag { name, args, known })
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn leading_spaces(s: &str) -> usize {
s.as_bytes()
.iter()
.take_while(|&&b| b == b' ' || b == b'\t')
.count()
}
fn find_tag_start(s: &str) -> Option<usize> {
let mut in_args = false;
for (i, &b) in s.as_bytes().iter().enumerate() {
match b {
b'(' => in_args = true,
b')' => in_args = false,
b'\\' if !in_args => return Some(i),
_ => {}
}
}
None
}
fn match_tag_name(after: &str) -> (&str, bool) {
let bytes = after.as_bytes();
for name in KNOWN_TAGS {
if bytes.starts_with(name.as_bytes()) {
return (&after[..name.len()], true);
}
}
let len = bytes.iter().take_while(|b| b.is_ascii_alphabetic()).count();
(&after[..len], false)
}
#[derive(Debug, Clone, Logos)]
enum RawTextToken<'a> {
#[regex(r"\{[^}]*\}")]
Override(&'a str),
#[token("\\N")]
HardBreak,
#[token("\\n")]
SoftBreak,
#[token("\\h")]
HardSpace,
#[token("\\{")]
#[token("\\}")]
EscapedBrace(&'a str),
#[token("\\")]
LoneBackslash,
#[token("{")]
LiteralBrace(&'a str),
#[regex(r"[^{\\]+")]
Text(&'a str),
}
#[derive(Debug, Clone, Logos)]
enum TailToken<'a> {
#[token("\\N")]
HardBreak,
#[token("\\n")]
SoftBreak,
#[token("\\h")]
HardSpace,
#[token("\\{")]
#[token("\\}")]
EscapedBrace(&'a str),
#[token("\\")]
LoneBackslash,
#[regex(r"[^\\]+")]
Text(&'a str),
}
enum Lexeme<'a> {
Text(&'a str),
HardBreak(&'a str),
SoftBreak(&'a str),
HardSpace(&'a str),
EscapedBrace(&'a str),
Override(&'a str),
}
impl<'a> RawTextToken<'a> {
fn into_lexeme(self, slice: &'a str) -> Lexeme<'a> {
match self {
Self::Text(run) | Self::LiteralBrace(run) => Lexeme::Text(run),
Self::LoneBackslash => Lexeme::Text(slice),
Self::HardBreak => Lexeme::HardBreak(slice),
Self::SoftBreak => Lexeme::SoftBreak(slice),
Self::HardSpace => Lexeme::HardSpace(slice),
Self::EscapedBrace(escape) => Lexeme::EscapedBrace(escape),
Self::Override(block) => Lexeme::Override(block),
}
}
}
impl<'a> TailToken<'a> {
fn into_lexeme(self, slice: &'a str) -> Lexeme<'a> {
match self {
Self::Text(run) => Lexeme::Text(run),
Self::LoneBackslash => Lexeme::Text(slice),
Self::HardBreak => Lexeme::HardBreak(slice),
Self::SoftBreak => Lexeme::SoftBreak(slice),
Self::HardSpace => Lexeme::HardSpace(slice),
Self::EscapedBrace(escape) => Lexeme::EscapedBrace(escape),
}
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn last_close_brace(input: &str) -> Option<usize> {
let bytes = input.as_bytes();
#[cfg(all(feature = "memchr", not(miri)))]
{
memchr::memrchr(b'}', bytes)
}
#[cfg(not(all(feature = "memchr", not(miri))))]
{
bytes.iter().rposition(|&b| b == b'}')
}
}
#[derive(Clone)]
pub struct TextParser<'a> {
input: &'a str,
head: logos::Lexer<'a, RawTextToken<'a>>,
tail: Option<logos::Lexer<'a, TailToken<'a>>>,
last_close: Option<usize>,
head_end: usize,
drawing: bool,
}
impl<'a> TextParser<'a> {
#[cfg_attr(not(tarpaulin), inline(always))]
pub fn new(input: &'a str) -> Self {
Self {
input,
head: RawTextToken::lexer(input),
tail: None,
last_close: last_close_brace(input),
head_end: 0,
drawing: false,
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn is_drawing(&self) -> bool {
self.drawing
}
#[cfg_attr(not(tarpaulin), inline(always))]
const fn literal(&self, run: &'a str) -> TextToken<'a> {
if self.drawing {
TextToken::Drawing(run)
} else {
TextToken::Text(run)
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
fn blocks_possible(&self) -> bool {
matches!(self.last_close, Some(last) if self.head_end <= last)
}
fn emit(&mut self, lexeme: Lexeme<'a>) -> TextToken<'a> {
match lexeme {
Lexeme::Text(run) => self.literal(run),
Lexeme::HardBreak(slice) if self.drawing => TextToken::Drawing(slice),
Lexeme::SoftBreak(slice) if self.drawing => TextToken::Drawing(slice),
Lexeme::HardSpace(slice) if self.drawing => TextToken::Drawing(slice),
Lexeme::EscapedBrace(slice) if self.drawing => TextToken::Drawing(slice),
Lexeme::HardBreak(_) => TextToken::HardBreak,
Lexeme::SoftBreak(_) => TextToken::SoftBreak,
Lexeme::HardSpace(_) => TextToken::HardSpace,
Lexeme::EscapedBrace(slice) => TextToken::EscapedBrace(&slice[1..]),
Lexeme::Override(slice) => {
let block = Override::new(&slice[1..slice.len() - 1]);
if let Some(scale) = block.drawing_scale() {
self.drawing = scale > 0;
}
TextToken::Override(block)
}
}
}
}
impl<'a> Iterator for TextParser<'a> {
type Item = TextToken<'a>;
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.tail.is_none() && !self.blocks_possible() {
self.tail = Some(TailToken::lexer(&self.input[self.head_end..]));
}
let lexeme = match self.tail.as_mut() {
Some(tail) => {
let token = tail.next()?;
let slice = tail.slice();
match token {
Ok(token) => token.into_lexeme(slice),
Err(()) => continue,
}
}
None => {
let token = self.head.next()?;
let slice = self.head.slice();
self.head_end = self.head.span().end;
match token {
Ok(token) => token.into_lexeme(slice),
Err(()) => continue,
}
}
};
return Some(self.emit(lexeme));
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Segment<'a> {
Text(&'a str),
HardBreak,
SoftBreak,
HardSpace,
}
#[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) | TextToken::EscapedBrace(run) => Segment::Text(run),
TextToken::HardBreak => Segment::HardBreak,
TextToken::SoftBreak => Segment::SoftBreak,
TextToken::HardSpace => Segment::HardSpace,
TextToken::Override(_) | TextToken::Drawing(_) => 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::HardBreak | Segment::SoftBreak => out.push('\n'),
Segment::HardSpace => out.push('\u{00A0}'),
}
}
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::HardBreak | Segment::SoftBreak => f.write_str("\n")?,
Segment::HardSpace => f.write_str("\u{00A0}")?,
}
}
Ok(())
}
}
}