use core::{fmt::Display, range::RangeInclusive};
use crate::tokens::{CharWithContext, Token};
pub(crate) fn trim_end_whitespace(s: &str) -> &str {
let end = s
.char_indices()
.map(Into::<CharWithContext>::into)
.rev()
.find_map(|CharWithContext(r, c)| (!c.is_whitespace()).then_some(r.end))
.unwrap_or_default();
&s[..end]
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)]
pub struct JsonChar(pub char);
#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)]
pub struct JsonByte(pub u8);
impl JsonByte {
pub(crate) fn is_whitespace(self) -> bool {
matches!(self.0, b' ' | b'\t' | b'\n' | b'\r')
}
pub(crate) fn as_token(self) -> Option<Token> {
let token = match self.0 {
b'{' => Token::OpenCurlyBrace,
b'}' => Token::ClosedCurlyBrace,
b':' => Token::Colon,
b',' => Token::Comma,
b'[' => Token::OpenSquareBracket,
b']' => Token::ClosedSquareBracket,
_ => return None,
};
Some(token)
}
}
impl JsonChar {
pub(crate) fn direct_escape(self) -> Option<&'static str> {
match self.0 {
'"' => Some(r#"\""#),
'\\' => Some(r"\\"),
'/' => Some(r"\/"),
'\u{0008}' => Some(r"\b"),
'\u{000C}' => Some(r"\f"),
'\n' => Some(r"\n"),
'\r' => Some(r"\r"),
'\t' => Some(r"\t"),
_ => None,
}
}
#[cfg(any(test, feature = "serde"))]
pub(crate) fn minimal_escape(self) -> Option<&'static str> {
self.direct_escape().filter(|escaped| *escaped != r"\/")
}
pub(crate) fn escape(self) -> String {
match self.direct_escape() {
Some(escaped) => escaped.into(),
None => format!("\\u{:04X}", u32::from(self.0)),
}
}
pub(crate) fn is_hexdigit(self) -> bool {
self.0.is_ascii_hexdigit()
}
pub(crate) fn can_be_escaped_directly(self) -> bool {
matches!(self.0, '"' | '\\' | '/' | 'b' | 'f' | 'n' | 'r' | 't')
}
pub(crate) const CONTROL_RANGE: RangeInclusive<char> = '\u{0000}'..='\u{001F}';
pub(crate) fn is_control(self) -> bool {
Self::CONTROL_RANGE.contains(&self.0)
}
pub(crate) fn is_whitespace(self) -> bool {
matches!(self.0, ' ' | '\t' | '\n' | '\r')
}
pub(crate) fn is_structural(self) -> bool {
matches!(self.0, '{' | '}' | '[' | ']' | ':' | ',')
}
}
impl Display for JsonChar {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let rep = if self.is_control() {
self.escape()
} else {
self.0.to_string()
};
write!(f, "{rep}")
}
}
impl From<char> for JsonChar {
fn from(value: char) -> Self {
Self(value)
}
}
impl From<u8> for JsonByte {
fn from(value: u8) -> Self {
Self(value)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case('"', Some(r#"\""#))]
#[case('\\', Some(r"\\"))]
#[case('/', Some(r"\/"))]
#[case('\n', Some(r"\n"))]
#[case('a', None)]
fn direct_escape_cases(#[case] input: char, #[case] expected: Option<&str>) {
assert_eq!(JsonChar(input).direct_escape(), expected);
}
#[rstest]
#[case('"', Some(r#"\""#))]
#[case('\\', Some(r"\\"))]
#[case('/', None)]
#[case('\n', Some(r"\n"))]
#[case('a', None)]
fn minimal_escape_cases(#[case] input: char, #[case] expected: Option<&str>) {
assert_eq!(JsonChar(input).minimal_escape(), expected);
}
#[rstest]
#[case('\u{0008}', r"\b")]
#[case('\u{000C}', r"\f")]
#[case('\n', r"\n")]
#[case('\r', r"\r")]
#[case('\t', r"\t")]
fn escape_char_for_json_string_short_forms(#[case] input: char, #[case] expected: &str) {
assert_eq!(JsonChar(input).to_string(), expected);
}
#[rstest]
#[case('\u{0000}', "\\u0000")]
#[case('\u{001F}', "\\u001F")]
#[case('\u{0011}', "\\u0011")]
fn escape_char_for_json_string_control_chars(#[case] input: char, #[case] expected: &str) {
assert_eq!(JsonChar(input).to_string(), expected);
}
#[rstest]
#[case('a')]
#[case('Z')]
#[case('0')]
#[case(' ')]
#[case('{')]
#[case('"')]
#[case('\\')]
#[case('/')]
fn escape_char_for_json_string_leaves_non_specials(#[case] input: char) {
assert_eq!(JsonChar(input).to_string(), input.to_string());
}
#[rstest]
#[case("h \t\n\r", "h")]
#[case("\u{000B} h ", "\u{000B} h")]
#[case("rust", "rust")]
fn trim_end_whitespace_cases(#[case] input: &str, #[case] output: &str) {
assert_eq!(trim_end_whitespace(input), output);
}
}