charming 0.6.0

A visualization library for Rust
Documentation
use serde::{Deserialize, Deserializer, Serialize};

pub static RAW_MARK: &str = "#*#*#*#";

#[derive(Debug, PartialEq, PartialOrd, Clone)]
pub struct RawString(String);

impl Serialize for RawString {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(format!("{}{}{}", RAW_MARK, self.0, RAW_MARK).as_str())
    }
}

impl<'de> Deserialize<'de> for RawString {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        struct RawStringVisitor;

        impl serde::de::Visitor<'_> for RawStringVisitor {
            type Value = RawString;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                formatter.write_str("a string wrapped with special RAW_MARK")
            }

            fn visit_str<E>(self, value: &str) -> Result<RawString, E>
            where
                E: serde::de::Error,
            {
                let mark_len = RAW_MARK.len();
                if value.starts_with(RAW_MARK)
                    && value.ends_with(RAW_MARK)
                    && value.len() >= 2 * mark_len
                {
                    let core = &value[mark_len..value.len() - mark_len];
                    Ok(RawString(core.to_string()))
                } else {
                    Err(E::custom(format!(
                        "string does not start and end with {}",
                        RAW_MARK
                    )))
                }
            }
        }

        deserializer.deserialize_str(RawStringVisitor)
    }
}

impl<S> From<S> for RawString
where
    S: Into<String>,
{
    fn from(s: S) -> Self {
        RawString(s.into())
    }
}

pub(crate) fn process_raw_strings(s: &str) -> String {
    let left_mark = format!("\"{RAW_MARK}");
    let right_mark = format!("{RAW_MARK}\"");

    let mut output = String::with_capacity(s.len());
    let mut pos = 0;

    while pos < s.len() {
        let left = pos + s[pos..].find(&left_mark).unwrap_or_else(|| s.len() - pos);
        output.push_str(&s[pos..left]);

        if left >= s.len() {
            break;
        }

        pos = left + left_mark.len();
        let right = pos + s[pos..].find(&right_mark).unwrap_or_else(|| s.len() - pos);
        output.push_str(&unescape_string(&s[pos..right]));

        pos = right + right_mark.len();
    }

    output
}

fn unescape_string(s: &str) -> String {
    let mut unescaped = String::with_capacity(s.len());
    let mut chars = s.chars().peekable();

    while let Some(c) = chars.next() {
        if c == '\\' && chars.peek().is_some() {
            unescaped.push(match chars.next().unwrap() {
                '\\' => '\\',
                '"' => '"',
                '\'' => '\'',
                '/' => '/',
                'b' => '\x08',
                'f' => '\x0c',
                'n' => '\n',
                'r' => '\r',
                't' => '\t',
                'v' => '\x0b',
                c => c,
            })
        } else {
            unescaped.push(c);
        }
    }

    unescaped
}

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

    #[test]
    fn raw_strings() {
        let s = format!("\"{}foobar{}\"", RAW_MARK, RAW_MARK);
        assert_eq!(process_raw_strings(&s), "foobar");

        let s = format!("foo\"{}bar{}\"baz", RAW_MARK, RAW_MARK);
        assert_eq!(process_raw_strings(&s), "foobarbaz");

        let s = format!("foo\"{}b\\na\\nr{}\"baz", RAW_MARK, RAW_MARK);
        assert_eq!(process_raw_strings(&s), "foob\na\nrbaz");
    }
}