use crate::error::{Error, ReasonCode, Result};
use crate::parser::classify;
use crate::value::Value;
use crate::whitespace::{
common_leading_whitespace_prefix_len, is_inline_whitespace, is_ktav_whitespace,
};
pub(super) const INDENT: &str = " ";
pub(crate) fn first_item_needs_wrap(item: &Value) -> bool {
matches!(item, Value::Object(_) | Value::Array(_))
}
fn push_unicode_escape(out: &mut String, ch: char) {
out.push_str(&format!("\\u{:04X}", ch as u32));
}
pub(crate) fn push_escaped_key_segment(key: &str, root_first_key: bool, out: &mut String) {
if key_segment_needs_quotes(key, root_first_key) {
push_quoted_key(key, out);
} else {
push_bare_key(key, out);
}
}
fn key_segment_needs_quotes(key: &str, root_first_key: bool) -> bool {
let first = key.chars().next();
let last = key.chars().next_back();
if matches!(first, Some('"') | Some('\'') | Some('`')) {
return true;
}
if root_first_key && first == Some('\u{FEFF}') {
return true;
}
if key.as_bytes().starts_with(b"##") {
return true;
}
if key.bytes().any(|b| {
matches!(
b,
b'.' | b':' | b',' | b'{' | b'}' | b'[' | b']' | b'(' | b')'
)
}) {
return true;
}
let edge_ws = |c: Option<char>| c.is_some_and(is_inline_whitespace);
edge_ws(first) || edge_ws(last)
}
fn push_bare_key(key: &str, out: &mut String) {
let first_ch = key.chars().next();
let last_ch = key.chars().next_back();
let edge_ws = |c: Option<char>| c.is_some_and(is_inline_whitespace);
let needs_rewrite = key.bytes().any(|b| {
matches!(
b,
b'\\' | b'.' | b':' | b',' | b'{' | b'}' | b'[' | b']' | b'(' | b')' | b'\n' | b'\r'
) || (b < 0x20 && !matches!(b, b'\t' | 0x0B | 0x0C))
|| b == 0x7F
}) || edge_ws(first_ch)
|| edge_ws(last_ch);
if !needs_rewrite {
out.push_str(key);
return;
}
out.reserve(key.len() + 8);
let last_idx = key.char_indices().next_back().map(|(i, _)| i).unwrap_or(0);
for (i, ch) in key.char_indices() {
let at_edge = i == 0 || i == last_idx;
match ch {
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
c if is_ktav_whitespace(c) => {
if at_edge {
push_unicode_escape(out, c);
} else {
out.push(c);
}
}
'.' => out.push_str("\\."),
':' => out.push_str("\\:"),
',' => out.push_str("\\,"),
'{' => out.push_str("\\{"),
'}' => out.push_str("\\}"),
'[' => out.push_str("\\["),
']' => out.push_str("\\]"),
c if (c as u32) < 0x20 || (c as u32) == 0x7F => push_unicode_escape(out, c),
c => out.push(c),
}
}
}
fn push_quoted_key(key: &str, out: &mut String) {
out.push('"');
for ch in key.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
c if ((c as u32) < 0x20 && !matches!(c, '\t' | '\u{0B}' | '\u{0C}'))
|| (c as u32) == 0x7F =>
{
push_unicode_escape(out, c)
}
c => out.push(c),
}
}
out.push('"');
}
pub(crate) fn string_needs_multiline(s: &str) -> bool {
s.chars().next().is_some_and(is_ktav_whitespace)
|| s.chars().next_back().is_some_and(is_ktav_whitespace)
|| s.bytes().any(|b| b < 0x20 && b != b'\t')
}
pub(crate) fn cr_error() -> Error {
Error::Unrepresentable(ReasonCode::CRByte)
}
#[derive(Debug)]
pub(crate) enum MultilineForm {
Stripped,
Verbatim,
}
pub(crate) fn choose_multiline_form(s: &str, prefer_stripped: bool) -> Result<MultilineForm> {
let mut sole_single = false;
let mut sole_double = false;
let mut ws_only_line = false;
let mut indented_line = false;
let mut trailing_ws_line = false;
for line in s.split('\n') {
let trimmed = line.trim_matches(is_inline_whitespace);
match trimmed {
")" => sole_single = true,
"))" => sole_double = true,
_ => {}
}
if line.is_empty() {
continue;
}
if trimmed.is_empty() {
ws_only_line = true;
} else if line.starts_with(is_inline_whitespace) {
indented_line = true;
}
if line.trim_end_matches(is_inline_whitespace).len() != line.len() {
trailing_ws_line = true;
}
}
let stripped_safe = !sole_single && !ws_only_line && !indented_line && !trailing_ws_line;
let verbatim_ok = !sole_double;
if prefer_stripped && stripped_safe {
Ok(MultilineForm::Stripped)
} else if verbatim_ok {
Ok(MultilineForm::Verbatim)
} else {
let has_common_indent = common_leading_whitespace_prefix_len(s.split('\n')) != 0;
let stripped_lossless =
!sole_single && !ws_only_line && !has_common_indent && !trailing_ws_line;
if stripped_lossless {
Ok(MultilineForm::Stripped)
} else {
let code = if sole_single {
ReasonCode::BothFormsRequired
} else if trailing_ws_line {
ReasonCode::TrailingWhitespaceCollision
} else {
ReasonCode::LeadingWhitespaceCollision
};
Err(Error::Unrepresentable(code))
}
}
}
pub(crate) fn item_needs_raw_marker(s: &str) -> bool {
needs_raw_marker(s) || s.starts_with("##") || s.starts_with("::") || matches!(s, "]" | "}")
}
pub(crate) fn bare_item_is_pair_candidate(body: &str) -> bool {
crate::parser::classify::is_pair_shape(body)
}
pub(crate) fn needs_raw_marker(s: &str) -> bool {
match s.as_bytes().first() {
None => false,
Some(&b' ') | Some(&b'\t') => {
needs_raw_marker_slow(s.trim_start_matches(is_inline_whitespace))
}
Some(&b'{') | Some(&b'[') => true,
Some(_) => needs_raw_marker_content(s),
}
}
fn needs_raw_marker_content(s: &str) -> bool {
if matches!(s, "null" | "true" | "false" | "(" | "((" | "()" | "(())") {
return true;
}
if classify::matches_integer_grammar(s) || classify::matches_float_grammar(s) {
return true;
}
false
}
#[cold]
#[inline(never)]
fn needs_raw_marker_slow(t: &str) -> bool {
t.starts_with('{') || t.starts_with('[') || needs_raw_marker_content(t)
}
pub(super) fn push_indent(out: &mut String, level: usize) {
const SPACES: &str = " "; let mut remaining = level * INDENT.len();
if remaining == 0 {
return;
}
out.reserve(remaining);
while remaining > 0 {
let chunk = remaining.min(SPACES.len());
out.push_str(&SPACES[..chunk]);
remaining -= chunk;
}
}