mxp 1.2.0

Rust implementation of the MXP (Mud eXtension Protocol) standard
Documentation
use std::cell::Cell;
use std::fmt;
use std::num::NonZero;

use flagset::FlagSet;

use crate::{Dimension, RgbColor};

fn is_plain_word(s: &str) -> bool {
    const fn is_wordlike(c: u8) -> bool {
        matches!(c, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'-')
    }
    s.bytes().all(is_wordlike)
}

pub(crate) struct DelimAfterFirst {
    delim: &'static str,
    after_first: Cell<bool>,
}

impl DelimAfterFirst {
    pub const fn new(delim: &'static str) -> Self {
        Self {
            delim,
            after_first: Cell::new(false),
        }
    }
}

impl fmt::Display for DelimAfterFirst {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.after_first.replace(true) {
            f.write_str(self.delim)
        } else {
            Ok(())
        }
    }
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct Escape<'a>(pub &'a str);

impl fmt::Display for Escape<'_> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if is_plain_word(self.0) {
            return self.0.fmt(f);
        }
        let escaped = html_escape::encode_double_quoted_attribute(self.0);
        write!(f, "\"{escaped}\"")
    }
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct MaybeQuote<'a>(pub &'a str);

impl fmt::Display for MaybeQuote<'_> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let body = self
            .0
            .strip_prefix('&')
            .and_then(|s| s.strip_suffix(';'))
            .unwrap_or(self.0);
        if is_plain_word(body) {
            self.0.fmt(f)
        } else {
            write!(f, "\"{}\"", self.0)
        }
    }
}

pub(crate) trait DisplayArg {
    fn is_default(&self) -> bool {
        false
    }
    fn display(&self, f: &mut fmt::Formatter) -> fmt::Result;
}

struct DisplayAsArg<'a>(&'a dyn DisplayArg);
impl fmt::Display for DisplayAsArg<'_> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        self.0.display(f)
    }
}

#[derive(Copy, Clone)]
pub(crate) struct ElementFormatter<'a> {
    pub name: &'a str,
    pub arguments: &'a [&'a dyn DisplayArg],
    pub keywords: &'a [(&'a str, bool)],
}

impl fmt::Display for ElementFormatter<'_> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let Self {
            name,
            arguments,
            keywords,
        } = *self;
        let args = match arguments.iter().rposition(|arg| !arg.is_default()) {
            Some(i) => &arguments[..=i],
            None => &[],
        };

        if args.is_empty() && keywords.iter().all(|k| !k.1) {
            return write!(f, "<{name}>");
        }

        write!(f, "<{name}")?;
        for &arg in args {
            write!(f, " {}", DisplayAsArg(arg))?;
        }
        for &(keyword, set) in keywords {
            if set {
                write!(f, " {keyword}")?;
            }
        }
        f.write_str(">")?;
        Ok(())
    }
}

macro_rules! impl_display {
    ($t:ty) => {
        impl DisplayArg for $t {
            fn display(&self, f: &mut fmt::Formatter) -> fmt::Result {
                fmt::Display::fmt(self, f)
            }
        }
    };
}

impl_display!(RgbColor);
impl_display!(u8);
impl_display!(u16);
impl_display!(u32);
impl_display!(NonZero<u8>);
impl_display!(NonZero<u32>);
impl_display!(crate::screen::Align);
impl_display!(crate::elements::AudioRepetition);

impl DisplayArg for (Option<RgbColor>, FlagSet<crate::FontStyle>) {
    fn is_default(&self) -> bool {
        self.0.is_none() && self.1.is_empty()
    }

    fn display(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let delim = DelimAfterFirst::new(",");
        if let Some(color) = self.0 {
            write!(f, "{color}{delim}")?;
        }
        for style in self.1 {
            write!(f, "{delim}{style}")?;
        }
        Ok(())
    }
}

impl DisplayArg for &str {
    fn is_default(&self) -> bool {
        self.is_empty()
    }

    fn display(&self, f: &mut fmt::Formatter) -> fmt::Result {
        fmt::Display::fmt(&Escape(self), f)
    }
}

impl<T: DisplayArg> DisplayArg for Option<T> {
    fn is_default(&self) -> bool {
        self.is_none()
    }

    fn display(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Some(arg) => arg.display(f),
            None => f.write_str("\"\""),
        }
    }
}

impl<T: fmt::Display> DisplayArg for Dimension<T> {
    fn display(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "\"{self}\"")
    }
}

impl<T: DisplayArg + Eq> DisplayArg for (T, T) {
    fn is_default(&self) -> bool {
        self.0 == self.1
    }

    fn display(&self, f: &mut fmt::Formatter) -> fmt::Result {
        self.0.display(f)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn delim_after_first() {
        let delim = DelimAfterFirst::new(", ");
        let output = format!("{delim}A{delim}B{delim}C");
        assert_eq!(output, "A, B, C");
    }

    #[test]
    fn escape() {
        let output = Escape("this & \"that\"").to_string();
        assert_eq!(output, "\"this &amp; &quot;that&quot;\"");
    }
}