#![allow(dead_code)]
use super::scanner::ScalarStyle;
pub(crate) fn cook(style: ScalarStyle, raw: &str) -> String {
match style {
ScalarStyle::Plain => cook_plain(raw),
ScalarStyle::SingleQuoted => cook_single_quoted(raw),
ScalarStyle::DoubleQuoted => cook_double_quoted(raw),
ScalarStyle::Literal | ScalarStyle::Folded => raw.to_string(),
}
}
pub(crate) fn cook_plain(raw: &str) -> String {
if !raw.contains('\n') {
return raw.trim().to_string();
}
let mut pieces = Vec::new();
for line in raw.split('\n') {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
pieces.push(trimmed.to_string());
}
pieces.join(" ")
}
pub(crate) fn cook_single_quoted(raw: &str) -> String {
if raw.contains('\n') {
cook_single_quoted_multi_line(raw)
} else {
cook_single_quoted_single_line(raw)
}
}
pub(crate) fn cook_double_quoted(raw: &str) -> String {
if raw.contains('\n') {
cook_double_quoted_multi_line(raw)
} else {
cook_double_quoted_single_line(raw)
}
}
pub(crate) fn cook_single_quoted_multi_line(raw: &str) -> String {
let trimmed = raw.trim_start_matches([' ', '\t', '\n']);
let inner = strip_quoted_wrapper(trimmed, '\'');
let folded = fold_quoted_inner(&inner, false);
folded.replace("''", "'")
}
pub(crate) fn cook_double_quoted_multi_line(raw: &str) -> String {
let trimmed = raw.trim_start_matches([' ', '\t', '\n']);
let inner = strip_quoted_wrapper(trimmed, '"');
let folded = fold_quoted_inner(&inner, true);
decode_double_quoted_inner(&folded)
}
pub(crate) fn cook_single_quoted_single_line(raw: &str) -> String {
let body = raw.strip_prefix('\'').unwrap_or(raw);
let body = body.strip_suffix('\'').unwrap_or(body);
body.replace("''", "'")
}
pub(crate) fn cook_double_quoted_single_line(raw: &str) -> String {
let body = raw.strip_prefix('"').unwrap_or(raw);
let mut out = String::with_capacity(body.len());
let mut chars = body.chars();
while let Some(ch) = chars.next() {
if ch == '"' {
break;
}
if ch != '\\' {
out.push(ch);
continue;
}
let Some(next) = chars.next() else {
out.push('\\');
break;
};
decode_double_quoted_escape(next, &mut chars, &mut out);
}
out
}
pub(crate) fn strip_quoted_wrapper(text: &str, quote: char) -> String {
let body = text.strip_prefix(quote).unwrap_or(text);
let mut out = String::with_capacity(body.len());
let mut chars = body.chars().peekable();
while let Some(ch) = chars.next() {
if quote == '"' {
if ch == '\\' {
out.push(ch);
if let Some(next) = chars.next() {
out.push(next);
}
continue;
}
if ch == '"' {
break;
}
} else if ch == '\'' {
if chars.peek() == Some(&'\'') {
out.push('\'');
out.push('\'');
chars.next();
continue;
}
break;
}
out.push(ch);
}
out
}
pub(crate) fn fold_quoted_inner(inner: &str, escaped_breaks: bool) -> String {
let mut out = String::new();
let mut blanks = 0usize;
let mut have_first = false;
for (idx, line) in inner.split('\n').enumerate() {
if idx == 0 {
out.push_str(line);
have_first = true;
continue;
}
let stripped = line.trim_start_matches([' ', '\t']);
if stripped.is_empty() {
blanks += 1;
continue;
}
trim_trailing_ws_respecting_escape(&mut out, escaped_breaks);
if escaped_breaks && blanks == 0 && have_first && ends_with_odd_backslashes(&out) {
out.pop();
out.push_str(stripped);
blanks = 0;
continue;
}
if !have_first {
} else if blanks == 0 {
out.push(' ');
} else {
for _ in 0..blanks {
out.push('\n');
}
}
out.push_str(stripped);
blanks = 0;
have_first = true;
}
if blanks > 0 {
trim_trailing_ws_respecting_escape(&mut out, escaped_breaks);
if blanks == 1 {
out.push(' ');
} else {
for _ in 0..blanks - 1 {
out.push('\n');
}
}
}
out
}
pub(crate) fn trim_trailing_ws_respecting_escape(out: &mut String, escaped_breaks: bool) {
let bytes = out.as_bytes();
let mut end = bytes.len();
while end > 0 && (bytes[end - 1] == b' ' || bytes[end - 1] == b'\t') {
end -= 1;
}
if !escaped_breaks || end == bytes.len() || end == 0 || bytes[end - 1] != b'\\' {
out.truncate(end);
return;
}
let mut bs_start = end - 1;
while bs_start > 0 && bytes[bs_start - 1] == b'\\' {
bs_start -= 1;
}
let bs_count = end - bs_start;
if bs_count % 2 == 1 {
out.truncate(end + 1);
} else {
out.truncate(end);
}
}
pub(crate) fn ends_with_odd_backslashes(s: &str) -> bool {
s.chars().rev().take_while(|&c| c == '\\').count() % 2 == 1
}
pub(crate) fn decode_double_quoted_inner(body: &str) -> String {
let mut out = String::with_capacity(body.len());
let mut chars = body.chars();
while let Some(ch) = chars.next() {
if ch != '\\' {
out.push(ch);
continue;
}
let Some(next) = chars.next() else {
out.push('\\');
break;
};
decode_double_quoted_escape(next, &mut chars, &mut out);
}
out
}
fn decode_double_quoted_escape(next: char, chars: &mut std::str::Chars<'_>, out: &mut String) {
match next {
'0' => out.push('\0'),
'a' => out.push('\u{07}'),
'b' => out.push('\u{08}'),
't' | '\t' => out.push('\t'),
'n' => out.push('\n'),
'v' => out.push('\u{0B}'),
'f' => out.push('\u{0C}'),
'r' => out.push('\r'),
'e' => out.push('\u{1B}'),
' ' => out.push(' '),
'"' => out.push('"'),
'/' => out.push('/'),
'\\' => out.push('\\'),
'N' => out.push('\u{85}'),
'_' => out.push('\u{A0}'),
'L' => out.push('\u{2028}'),
'P' => out.push('\u{2029}'),
'x' => {
if let Some(c) = take_hex_char(chars, 2) {
out.push(c);
}
}
'u' => {
if let Some(c) = take_hex_char(chars, 4) {
out.push(c);
}
}
'U' => {
if let Some(c) = take_hex_char(chars, 8) {
out.push(c);
}
}
other => {
out.push('\\');
out.push(other);
}
}
}
fn take_hex_char(chars: &mut std::str::Chars<'_>, n: usize) -> Option<char> {
let hex: String = chars.take(n).collect();
if hex.len() != n {
return None;
}
u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_single_line_trims_whitespace() {
assert_eq!(cook_plain(" hello "), "hello");
}
#[test]
fn plain_multi_line_folds_with_single_space() {
assert_eq!(cook_plain("hello\n world"), "hello world");
}
#[test]
fn plain_multi_line_skips_blank_lines() {
assert_eq!(cook_plain("a\n\nb"), "a b");
}
#[test]
fn plain_multi_line_skips_hash_comment_lines() {
assert_eq!(cook_plain("a\n# comment\nb"), "a b");
}
#[test]
fn single_quoted_single_line_unescapes_doubled_quote() {
assert_eq!(cook_single_quoted("'it''s'"), "it's");
}
#[test]
fn double_quoted_single_line_decodes_basic_escapes() {
assert_eq!(cook_double_quoted("\"a\\nb\""), "a\nb");
}
#[test]
fn double_quoted_decodes_hex_escape() {
assert_eq!(cook_double_quoted("\"\\x41\""), "A");
}
#[test]
fn double_quoted_decodes_unicode_escape() {
assert_eq!(cook_double_quoted("\"\\u00e9\""), "é");
}
#[test]
fn double_quoted_unknown_escape_kept_verbatim() {
assert_eq!(cook_double_quoted("\"\\?\""), "\\?");
}
#[test]
fn single_quoted_multi_line_folds_lines_and_unescapes() {
assert_eq!(cook_single_quoted("'foo\n bar''baz'"), "foo bar'baz");
}
#[test]
fn double_quoted_multi_line_folds_and_decodes() {
assert_eq!(cook_double_quoted("\"foo\n bar\\n\""), "foo bar\n");
}
#[test]
fn cook_dispatches_on_style() {
assert_eq!(cook(ScalarStyle::Plain, " x "), "x");
assert_eq!(cook(ScalarStyle::SingleQuoted, "'x'"), "x");
assert_eq!(cook(ScalarStyle::DoubleQuoted, "\"x\""), "x");
assert_eq!(cook(ScalarStyle::Literal, "|\n x\n"), "|\n x\n");
assert_eq!(cook(ScalarStyle::Folded, ">\n x\n"), ">\n x\n");
}
}