1use super::resolve::all_digits;
4use super::{CoreScalarType, ScalarStyle, ScalarValue, INDICATORS};
5
6fn push_unicode_escape(
12 out: &mut String,
13 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
14 prefix: char,
15 width: usize,
16) {
17 let hex_digits: String = chars.by_ref().take(width).collect();
18 let decoded = if hex_digits.len() == width && hex_digits.chars().all(|c| c.is_ascii_hexdigit())
20 {
21 u32::from_str_radix(&hex_digits, 16)
22 .ok()
23 .and_then(char::from_u32)
24 } else {
25 None
26 };
27 match decoded {
28 Some(c) => out.push(c),
29 None => {
30 out.push('\\');
31 out.push(prefix);
32 out.push_str(&hex_digits);
33 }
34 }
35}
36
37fn skip_line_prefixes(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> usize {
40 let mut empty_lines = 0;
41 loop {
42 while chars.next_if(|c| *c == ' ' || *c == '\t').is_some() {}
43 match chars.next_if(|c| *c == '\n' || *c == '\r') {
44 Some('\r') => {
45 chars.next_if_eq(&'\n');
46 }
47 Some(_) => {}
48 None => return empty_lines,
49 }
50 empty_lines += 1;
51 }
52}
53
54impl ScalarValue {
55 pub fn parse_escape_sequences(text: &str) -> String {
58 let mut result = String::with_capacity(text.len()); let mut chars = text.chars().peekable();
60 let mut pending_white = String::new();
62
63 while let Some(ch) = chars.next() {
64 if ch == '\n' || ch == '\r' {
65 if ch == '\r' {
66 chars.next_if_eq(&'\n');
67 }
68 pending_white.clear();
69 match skip_line_prefixes(&mut chars) {
70 0 => result.push(' '),
71 n => result.push_str(&"\n".repeat(n)),
72 }
73 continue;
74 }
75 if ch == ' ' || ch == '\t' {
76 pending_white.push(ch);
77 continue;
78 }
79 result.push_str(&pending_white);
80 pending_white.clear();
81 if ch == '\\' {
82 if let Some(&escaped) = chars.peek() {
83 chars.next(); match escaped {
85 'n' => result.push('\n'),
87 't' | '\t' => result.push('\t'),
88 'r' => result.push('\r'),
89 'b' => result.push('\x08'),
90 'f' => result.push('\x0C'),
91 'a' => result.push('\x07'), 'e' => result.push('\x1B'), 'v' => result.push('\x0B'), '0' => result.push('\0'), '\\' => result.push('\\'),
96 '"' => result.push('"'),
97 '\'' => result.push('\''),
98 '/' => result.push('/'),
99 'N' => result.push('\u{85}'),
100 '_' => result.push('\u{A0}'),
101 'L' => result.push('\u{2028}'),
102 'P' => result.push('\u{2029}'),
103 ' ' => result.push(' '),
104 '\n' | '\r' => {
105 if escaped == '\r' {
108 chars.next_if_eq(&'\n');
109 }
110 let empty_lines = skip_line_prefixes(&mut chars);
111 result.push_str(&"\n".repeat(empty_lines));
112 }
113 'x' => push_unicode_escape(&mut result, &mut chars, 'x', 2),
114 'u' => push_unicode_escape(&mut result, &mut chars, 'u', 4),
115 'U' => push_unicode_escape(&mut result, &mut chars, 'U', 8),
116 _ => {
118 result.push('\\');
119 result.push(escaped);
120 }
121 }
122 } else {
123 result.push('\\');
125 }
126 } else {
127 result.push(ch);
128 }
129 }
130 result.push_str(&pending_white);
131
132 result
133 }
134
135 pub(super) fn detect_style(value: &str) -> ScalarStyle {
137 if Self::needs_double_quotes(value) {
144 return ScalarStyle::DoubleQuoted;
145 }
146 if value.contains('\n') {
147 if value
150 .split('\n')
151 .any(|line| !line.is_empty() && line.trim_matches([' ', '\t']).is_empty())
152 {
153 return ScalarStyle::DoubleQuoted;
154 }
155 return ScalarStyle::Literal;
156 }
157 if Self::needs_quoting(value) {
159 if !value.contains('\'') {
161 ScalarStyle::SingleQuoted
162 } else {
163 ScalarStyle::DoubleQuoted
164 }
165 } else {
166 ScalarStyle::Plain
167 }
168 }
169
170 pub(super) fn needs_double_quotes(value: &str) -> bool {
177 value.chars().any(|c| {
178 !matches!(c, '\t' | '\n' | '\x20'..='\x7E' | '\u{A0}'..='\u{FFFD}' | '\u{10000}'..)
179 || matches!(c, '\u{2028}' | '\u{2029}' | '\u{FEFF}')
180 })
181 }
182
183 pub(super) fn needs_quoting(value: &str) -> bool {
185 if value.is_empty() {
187 return true;
188 }
189 Self::needs_double_quotes(value)
190 || Self::resolves_to_non_string(value)
191 || Self::needs_quoting_as_tagged(value)
192 }
193
194 fn resolves_to_non_string(value: &str) -> bool {
197 if Self::classify_plain(value) != CoreScalarType::String {
200 return true;
201 }
202
203 if value.eq_ignore_ascii_case("true")
206 || value.eq_ignore_ascii_case("false")
207 || value.eq_ignore_ascii_case("yes")
208 || value.eq_ignore_ascii_case("no")
209 || value.eq_ignore_ascii_case("on")
210 || value.eq_ignore_ascii_case("off")
211 || value.eq_ignore_ascii_case("null")
212 || value == "~"
213 {
214 return true;
215 }
216
217 let unsigned = value.strip_prefix(['+', '-']).unwrap_or(value);
221 let radix_like = ["0b", "0B", "0o", "0O", "0x", "0X"]
222 .iter()
223 .any(|p| unsigned.strip_prefix(p).is_some_and(|d| all_digits(d, 16)));
224 value.parse::<f64>().is_ok() || radix_like
225 }
226
227 pub(super) fn needs_quoting_as_tagged(value: &str) -> bool {
230 if value.starts_with(INDICATORS) || value.starts_with('<') {
235 return true;
236 }
237
238 if value.starts_with("...") || value.starts_with("---") {
242 return true;
243 }
244
245 let mut prev: Option<char> = None;
251 let mut chars = value.chars().peekable();
252 while let Some(ch) = chars.next() {
253 match ch {
254 '&' | '*' | '!' | '|' | '\'' | '"' | '%' => return true,
255 ':' if chars.peek().map_or(true, |next| next.is_whitespace()) => {
256 return true;
257 }
258 '#' if prev.map_or(true, char::is_whitespace) => {
259 return true;
260 }
261 _ => {}
262 }
263 prev = Some(ch);
264 }
265
266 if value != value.trim() {
268 return true;
269 }
270
271 false
272 }
273
274 pub(super) fn to_single_quoted(&self, indent: usize) -> String {
277 if self.value.contains('\r') {
280 return self.to_double_quoted();
281 }
282 let white = [' ', '\t'];
285 if self
286 .value
287 .split('\n')
288 .skip(1)
289 .zip(self.value.split('\n'))
290 .any(|(next, prev)| next.starts_with(white) || prev.ends_with(white))
291 {
292 return self.to_double_quoted();
293 }
294 let mut result = String::from("'");
296 let mut prev_empty = false;
297 for (i, part) in self.value.split('\n').enumerate() {
298 if i > 0 {
299 result.push('\n');
300 if !prev_empty || i == 1 {
301 result.push('\n');
302 }
303 if !part.is_empty() {
304 result.push_str(&" ".repeat(indent));
305 }
306 }
307 result.push_str(&part.replace('\'', "''"));
308 prev_empty = part.is_empty();
309 }
310 result.push('\'');
311 result
312 }
313
314 pub(super) fn to_double_quoted(&self) -> String {
316 let mut result = String::from("\"");
317 for ch in self.value.chars() {
318 match ch {
319 '"' => result.push_str("\\\""),
320 '\\' => result.push_str("\\\\"),
321 '\n' => result.push_str("\\n"),
322 '\r' => result.push_str("\\r"),
323 '\t' => result.push_str("\\t"),
324 '\x08' => result.push_str("\\b"),
325 '\x0C' => result.push_str("\\f"),
326 '\x07' => result.push_str("\\a"), '\x1B' => result.push_str("\\e"), '\x0B' => result.push_str("\\v"), '\0' => result.push_str("\\0"), c if c.is_control() || (c as u32) > 0x7F => {
331 let code_point = c as u32;
333 if code_point <= 0xFF {
334 result.push_str(&format!("\\x{code_point:02X}"));
335 } else if code_point <= 0xFFFF {
336 result.push_str(&format!("\\u{code_point:04X}"));
337 } else {
338 result.push_str(&format!("\\U{code_point:08X}"));
339 }
340 }
341 c => result.push(c),
342 }
343 }
344 result.push('"');
345 result
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn test_escape_sequences_basic() {
355 assert_eq!(
357 ScalarValue::parse_escape_sequences("hello\\nworld"),
358 "hello\nworld"
359 );
360 assert_eq!(
361 ScalarValue::parse_escape_sequences("tab\\there"),
362 "tab\there"
363 );
364 assert_eq!(
365 ScalarValue::parse_escape_sequences("quote\\\"test"),
366 "quote\"test"
367 );
368 assert_eq!(
369 ScalarValue::parse_escape_sequences("back\\\\slash"),
370 "back\\slash"
371 );
372 assert_eq!(
373 ScalarValue::parse_escape_sequences("return\\rtest"),
374 "return\rtest"
375 );
376 }
377
378 #[test]
379 fn test_escape_sequences_control_chars() {
380 assert_eq!(ScalarValue::parse_escape_sequences("bell\\a"), "bell\x07");
382 assert_eq!(
383 ScalarValue::parse_escape_sequences("backspace\\b"),
384 "backspace\x08"
385 );
386 assert_eq!(
387 ScalarValue::parse_escape_sequences("formfeed\\f"),
388 "formfeed\x0C"
389 );
390 assert_eq!(
391 ScalarValue::parse_escape_sequences("escape\\e"),
392 "escape\x1B"
393 );
394 assert_eq!(ScalarValue::parse_escape_sequences("vtab\\v"), "vtab\x0B");
395 assert_eq!(ScalarValue::parse_escape_sequences("null\\0"), "null\0");
396 assert_eq!(ScalarValue::parse_escape_sequences("slash\\/"), "slash/");
397 }
398
399 #[test]
400 fn test_escape_sequences_unicode_x() {
401 assert_eq!(ScalarValue::parse_escape_sequences("\\x41"), "A"); assert_eq!(ScalarValue::parse_escape_sequences("\\x7A"), "z"); assert_eq!(ScalarValue::parse_escape_sequences("\\x20"), " "); assert_eq!(ScalarValue::parse_escape_sequences("\\xFF"), "\u{FF}"); assert_eq!(ScalarValue::parse_escape_sequences("\\xGH"), "\\xGH"); assert_eq!(ScalarValue::parse_escape_sequences("\\x4"), "\\x4"); assert_eq!(ScalarValue::parse_escape_sequences("\\x4Z"), "\\x4Z");
411 assert_eq!(ScalarValue::parse_escape_sequences("\\xAZ"), "\\xAZ");
412 assert_eq!(ScalarValue::parse_escape_sequences("\\x+4"), "\\x+4");
413 assert_eq!(ScalarValue::parse_escape_sequences("\\u+041"), "\\u+041");
414 }
415
416 #[test]
417 fn test_escape_sequences_unicode_u() {
418 assert_eq!(ScalarValue::parse_escape_sequences("\\u0041"), "A"); assert_eq!(ScalarValue::parse_escape_sequences("\\u03B1"), "Ξ±"); assert_eq!(ScalarValue::parse_escape_sequences("\\u2603"), "β"); assert_eq!(ScalarValue::parse_escape_sequences("\\u4E2D"), "δΈ"); assert_eq!(ScalarValue::parse_escape_sequences("\\uGHIJ"), "\\uGHIJ"); assert_eq!(ScalarValue::parse_escape_sequences("\\u041"), "\\u041"); }
428
429 #[test]
430 fn test_escape_sequences_unicode_capital_u() {
431 assert_eq!(ScalarValue::parse_escape_sequences("\\U00000041"), "A"); assert_eq!(ScalarValue::parse_escape_sequences("\\U0001F603"), "π"); assert_eq!(ScalarValue::parse_escape_sequences("\\U0001F4A9"), "π©"); assert_eq!(
438 ScalarValue::parse_escape_sequences("\\UGHIJKLMN"),
439 "\\UGHIJKLMN"
440 ); assert_eq!(
442 ScalarValue::parse_escape_sequences("\\U0000004"),
443 "\\U0000004"
444 ); assert_eq!(
446 ScalarValue::parse_escape_sequences("\\UFFFFFFFF"),
447 "\\UFFFFFFFF"
448 ); }
450
451 #[test]
452 fn test_escape_sequences_line_folding() {
453 assert_eq!(
455 ScalarValue::parse_escape_sequences("line\\ \nfolding"),
456 "line folding"
457 );
458 assert_eq!(ScalarValue::parse_escape_sequences("a\\\n b"), "ab");
459 assert_eq!(ScalarValue::parse_escape_sequences("a\\\r\n b"), "ab");
460 assert_eq!(
461 ScalarValue::parse_escape_sequences("escaped\\nline\\nbreak"),
462 "escaped\nline\nbreak"
463 );
464 assert_eq!(
465 ScalarValue::parse_escape_sequences("remove\\\nline\\nbreak"),
466 "removeline\nbreak"
467 );
468 }
469
470 #[test]
471 fn test_escape_sequences_mixed() {
472 let input = "Hello\\nWorld\\u0021\\x20\\U0001F44D";
474 let expected = "Hello\nWorld! π";
475 assert_eq!(ScalarValue::parse_escape_sequences(input), expected);
476
477 let input = "Quote\\\"back\\\\slash\\ttab";
479 let expected = "Quote\"back\\slash\ttab";
480 assert_eq!(ScalarValue::parse_escape_sequences(input), expected);
481 }
482
483 #[test]
484 fn test_escape_sequences_yaml_specific() {
485 assert_eq!(
486 ScalarValue::parse_escape_sequences("\\N\\_\\L\\P\\\t"),
487 "\u{85}\u{A0}\u{2028}\u{2029}\t"
488 );
489 }
490
491 #[test]
492 fn test_escape_sequences_unknown() {
493 assert_eq!(ScalarValue::parse_escape_sequences("\\q"), "\\q");
495 assert_eq!(ScalarValue::parse_escape_sequences("\\z"), "\\z");
496 assert_eq!(ScalarValue::parse_escape_sequences("\\1"), "\\1");
497 }
498
499 #[test]
500 fn test_escape_sequences_edge_cases() {
501 assert_eq!(ScalarValue::parse_escape_sequences(""), "");
503 assert_eq!(ScalarValue::parse_escape_sequences("\\"), "\\");
504 assert_eq!(
505 ScalarValue::parse_escape_sequences("no escapes"),
506 "no escapes"
507 );
508 assert_eq!(ScalarValue::parse_escape_sequences("\\\\\\\\"), "\\\\");
509 }
510
511 #[test]
512 fn test_parse_escape_single_quote() {
513 assert_eq!(ScalarValue::parse_escape_sequences("a\\'b"), "a'b");
515 }
516}