use crate::error::{Error, Result};
use crate::parser::classify;
use crate::value::Value;
pub(super) const INDENT: &str = " ";
pub(crate) fn first_item_needs_wrap(item: &Value) -> bool {
matches!(item, Value::Object(_) | Value::Array(_))
}
pub(crate) fn push_escaped_key_segment(key: &str, out: &mut String) {
let bytes = key.as_bytes();
let needs_escape = bytes.iter().any(|&b| {
matches!(
b,
b'\\' | b',' | b'}' | b']' | b'{' | b'[' | b'\n' | b'\r' | b'.' | b':'
)
});
if !needs_escape {
out.push_str(key);
return;
}
out.reserve(key.len() + 4);
for ch in key.chars() {
match ch {
'\\' => out.push_str("\\\\"),
',' => out.push_str("\\,"),
'}' => out.push_str("\\}"),
']' => out.push_str("\\]"),
'{' => out.push_str("\\{"),
'[' => out.push_str("\\["),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'.' => out.push_str("\\."),
':' => out.push_str("\\:"),
other => out.push(other),
}
}
}
pub(crate) fn string_needs_multiline(s: &str) -> bool {
s.chars().next().is_some_and(char::is_whitespace)
|| s.chars().next_back().is_some_and(char::is_whitespace)
|| s.bytes().any(|b| b < 0x20 && b != b'\t')
}
pub(crate) fn cr_error() -> Error {
Error::Message(
"String containing CR (0x0D) is not representable in canonical form (§ 5.9.7)".into(),
)
}
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 unindented_line = false;
for line in s.split('\n') {
let trimmed = line.trim();
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(|c: char| c.is_whitespace()) {
indented_line = true;
} else {
unindented_line = true;
}
}
let stripped_safe = !sole_single && !ws_only_line && !indented_line;
let stripped_lossless = !sole_single && !ws_only_line && unindented_line;
let verbatim_ok = !sole_double;
if prefer_stripped && stripped_safe {
Ok(MultilineForm::Stripped)
} else if verbatim_ok {
Ok(MultilineForm::Verbatim)
} else if stripped_lossless {
Ok(MultilineForm::Stripped)
} else {
Err(Error::Message(
"String has no lossless multi-line form (§ 5.6.1): a sole-`))` \
content line closes the verbatim block, and the stripped block \
cannot hold this body (a sole-`)` line, a whitespace-only line, \
or every line indented). Split the value across adjacent \
multi-line pairs."
.into(),
))
}
}
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 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()),
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 s.starts_with('(') {
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;
}
}