use std::fmt;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
#[derive(EnumIter, Debug, Clone, Copy, PartialEq)]
pub enum StyleKind {
Bold,
Italic,
Underline,
Strikethrough,
Link,
}
impl StyleKind {
pub const fn to_tag(&self) -> &str {
match self {
StyleKind::Bold => "b",
StyleKind::Italic => "i",
StyleKind::Underline => "u",
StyleKind::Strikethrough => "s",
StyleKind::Link => "url",
}
}
}
#[derive(EnumIter, Debug, Clone, Copy, PartialEq)]
pub enum EmoteKind {
Smile,
Sad,
ColonD,
ColonThree,
Fearful,
Sunglasses,
Crying,
Winking,
}
impl EmoteKind {
pub const fn to_tag(&self) -> &str {
match self {
EmoteKind::Smile => ":)",
EmoteKind::Sad => ":(",
EmoteKind::ColonD => ":D",
EmoteKind::ColonThree => ":3",
EmoteKind::Fearful => "D:",
EmoteKind::Sunglasses => "B)",
EmoteKind::Crying => ";(",
EmoteKind::Winking => ";)",
}
}
pub const fn to_name(&self) -> &str {
match self {
EmoteKind::Smile => "smile",
EmoteKind::Sad => "sad",
EmoteKind::ColonD => "colond",
EmoteKind::ColonThree => "colonthree",
EmoteKind::Fearful => "fearful",
EmoteKind::Sunglasses => "sunglasses",
EmoteKind::Crying => "crying",
EmoteKind::Winking => "winking",
}
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Color {
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Part {
Text(String),
Escape,
Newline,
Style(StyleKind, bool),
Color(Color, bool),
Emote(EmoteKind),
}
impl Part {
fn parse_style_tag(mut body: &str) -> Option<Self> {
let mut enable = true;
if body.starts_with('/') {
enable = false;
body = &body[1..];
}
for style in StyleKind::iter() {
if body == style.to_tag() {
return Some(Self::Style(style, enable));
}
}
None
}
fn parse_emote_tag(body: &str) -> Option<Self> {
for emote in EmoteKind::iter() {
if body == emote.to_tag() {
return Some(Self::Emote(emote));
}
}
None
}
fn parse_color_tag(body: &str) -> Option<Self> {
if body.len() == 13 && body.starts_with("color=#") {
let r = u8::from_str_radix(&body[ 7.. 9], 16).ok()?;
let g = u8::from_str_radix(&body[ 9..11], 16).ok()?;
let b = u8::from_str_radix(&body[11..13], 16).ok()?;
Some(Self::Color(Color::new(r, g, b), true))
} else if body == "/color" {
Some(Self::Color(Color::default(), false))
} else {
None
}
}
fn parse_tag(body: &str) -> Option<Self> {
if body.is_empty() || body.len() > 32 {
return None;
}
Self::parse_style_tag(body)
.or_else(|| Self::parse_emote_tag(body))
.or_else(|| Self::parse_color_tag(body))
}
}
impl fmt::Display for Part {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Part::Text(text) => write!(f, "{text}"),
Part::Escape => write!(f, "\\"),
Part::Newline => write!(f, "\n"),
Part::Style(style, enable) => {
if *enable {
write!(f, "[{}]", style.to_tag())
} else {
write!(f, "[/{}]", style.to_tag())
}
}
Part::Color(color, enable) => {
if *enable {
write!(f, "[color={}]", color)
} else {
write!(f, "[/color]")
}
}
Part::Emote(emote) => write!(f, "[{}]", emote.to_tag()),
}
}
}
#[derive(Default, Debug)]
struct Parser {
parts: Vec<Part>,
buffer: String,
escape: bool,
}
impl Parser {
fn new() -> Self {
Self::default()
}
fn emit(&mut self, part: Part) {
self.parts.push(part);
}
fn flush(&mut self) {
if !self.buffer.is_empty() {
self.emit(Part::Text(self.buffer.clone()));
self.buffer.clear();
}
}
fn tag(&mut self) -> bool {
let index = match self.buffer.rfind('[') {
Some(index) => index,
None => return false,
};
if index == 0 && matches!(self.parts.last(), Some(Part::Escape)) {
return false;
}
let body = &self.buffer[index+1..];
let part = Part::parse_tag(body);
if let Some(part) = part {
self.buffer.drain(index..);
self.flush();
self.emit(part);
true
} else {
false
}
}
fn parse(mut self, input: &str) -> Vec<Part> {
for char in input.chars() {
if !self.escape {
if char == '\\' {
self.escape = true;
self.flush();
self.emit(Part::Escape);
continue;
}
if char == ']' {
if self.tag() {
continue;
}
}
}
self.escape = false;
if char == '\n' {
self.flush();
self.emit(Part::Newline);
continue;
}
self.buffer.push(char);
}
self.flush();
self.parts
}
}
pub fn parse(input: &str) -> Vec<Part> {
Parser::new().parse(input)
}
pub fn length(parts: &[Part]) -> usize {
parts.iter().fold(0, |acc, part| {
match part {
Part::Text(text) => acc + text.chars().count(),
Part::Newline | Part::Emote(_) => acc + 1,
_ => acc,
}
})
}