jjpwrgem_parse/tokens/
lexical.rs1use core::{fmt::Display, range::RangeInclusive};
2
3use crate::tokens::{CharWithContext, Token};
4
5pub(crate) fn trim_end_whitespace(s: &str) -> &str {
7 let end = s
8 .char_indices()
9 .map(Into::<CharWithContext>::into)
10 .rev()
11 .find_map(|CharWithContext(r, c)| (!c.is_whitespace()).then_some(r.end))
12 .unwrap_or_default();
13
14 &s[..end]
15}
16
17#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)]
18pub struct JsonChar(pub char);
19
20#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)]
21pub struct JsonByte(pub u8);
22
23impl JsonByte {
24 pub(crate) fn is_whitespace(self) -> bool {
25 matches!(self.0, b' ' | b'\t' | b'\n' | b'\r')
26 }
27
28 pub(crate) fn as_token(self) -> Option<Token> {
29 let token = match self.0 {
30 b'{' => Token::OpenCurlyBrace,
31 b'}' => Token::ClosedCurlyBrace,
32 b':' => Token::Colon,
33 b',' => Token::Comma,
34 b'[' => Token::OpenSquareBracket,
35 b']' => Token::ClosedSquareBracket,
36 _ => return None,
37 };
38 Some(token)
39 }
40}
41
42impl JsonChar {
43 pub(crate) fn direct_escape(self) -> Option<&'static str> {
63 match self.0 {
64 '"' => Some(r#"\""#),
65 '\\' => Some(r"\\"),
66 '/' => Some(r"\/"),
67 '\u{0008}' => Some(r"\b"),
68 '\u{000C}' => Some(r"\f"),
69 '\n' => Some(r"\n"),
70 '\r' => Some(r"\r"),
71 '\t' => Some(r"\t"),
72 _ => None,
73 }
74 }
75
76 #[cfg(any(test, feature = "serde"))]
77 pub(crate) fn minimal_escape(self) -> Option<&'static str> {
78 self.direct_escape().filter(|escaped| *escaped != r"\/")
79 }
80
81 pub(crate) fn escape(self) -> String {
82 match self.direct_escape() {
83 Some(escaped) => escaped.into(),
84 None => format!("\\u{:04X}", u32::from(self.0)),
85 }
86 }
87
88 pub(crate) fn is_hexdigit(self) -> bool {
89 self.0.is_ascii_hexdigit()
90 }
91
92 pub(crate) fn can_be_escaped_directly(self) -> bool {
93 matches!(self.0, '"' | '\\' | '/' | 'b' | 'f' | 'n' | 'r' | 't')
94 }
95
96 pub(crate) const CONTROL_RANGE: RangeInclusive<char> = '\u{0000}'..='\u{001F}';
98
99 pub(crate) fn is_control(self) -> bool {
101 Self::CONTROL_RANGE.contains(&self.0)
102 }
103
104 pub(crate) fn is_whitespace(self) -> bool {
114 matches!(self.0, ' ' | '\t' | '\n' | '\r')
115 }
116
117 pub(crate) fn is_structural(self) -> bool {
131 matches!(self.0, '{' | '}' | '[' | ']' | ':' | ',')
132 }
133}
134
135impl Display for JsonChar {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 let rep = if self.is_control() {
138 self.escape()
139 } else {
140 self.0.to_string()
141 };
142 write!(f, "{rep}")
143 }
144}
145
146impl From<char> for JsonChar {
147 fn from(value: char) -> Self {
148 Self(value)
149 }
150}
151
152impl From<u8> for JsonByte {
153 fn from(value: u8) -> Self {
154 Self(value)
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use rstest::rstest;
161
162 use super::*;
163
164 #[rstest]
165 #[case('"', Some(r#"\""#))]
166 #[case('\\', Some(r"\\"))]
167 #[case('/', Some(r"\/"))]
168 #[case('\n', Some(r"\n"))]
169 #[case('a', None)]
170 fn direct_escape_cases(#[case] input: char, #[case] expected: Option<&str>) {
171 assert_eq!(JsonChar(input).direct_escape(), expected);
172 }
173
174 #[rstest]
175 #[case('"', Some(r#"\""#))]
176 #[case('\\', Some(r"\\"))]
177 #[case('/', None)]
178 #[case('\n', Some(r"\n"))]
179 #[case('a', None)]
180 fn minimal_escape_cases(#[case] input: char, #[case] expected: Option<&str>) {
181 assert_eq!(JsonChar(input).minimal_escape(), expected);
182 }
183
184 #[rstest]
185 #[case('\u{0008}', r"\b")]
186 #[case('\u{000C}', r"\f")]
187 #[case('\n', r"\n")]
188 #[case('\r', r"\r")]
189 #[case('\t', r"\t")]
190 fn escape_char_for_json_string_short_forms(#[case] input: char, #[case] expected: &str) {
191 assert_eq!(JsonChar(input).to_string(), expected);
192 }
193
194 #[rstest]
195 #[case('\u{0000}', "\\u0000")]
196 #[case('\u{001F}', "\\u001F")]
197 #[case('\u{0011}', "\\u0011")]
198 fn escape_char_for_json_string_control_chars(#[case] input: char, #[case] expected: &str) {
199 assert_eq!(JsonChar(input).to_string(), expected);
200 }
201
202 #[rstest]
203 #[case('a')]
204 #[case('Z')]
205 #[case('0')]
206 #[case(' ')]
207 #[case('{')]
208 #[case('"')]
209 #[case('\\')]
210 #[case('/')]
211 fn escape_char_for_json_string_leaves_non_specials(#[case] input: char) {
212 assert_eq!(JsonChar(input).to_string(), input.to_string());
213 }
214
215 #[rstest]
216 #[case("h \t\n\r", "h")]
217 #[case("\u{000B} h ", "\u{000B} h")]
218 #[case("rust", "rust")]
219 fn trim_end_whitespace_cases(#[case] input: &str, #[case] output: &str) {
220 assert_eq!(trim_end_whitespace(input), output);
221 }
222}