use regex::Regex;
use std::sync::LazyLock;
static JSON_NUMBER_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$").unwrap());
static NUMERIC_LIKE_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[+-]\.?\d|^\.\d|^0\d").unwrap());
static BARE_KEY_RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[a-zA-Z_][a-zA-Z0-9_]*$").unwrap());
static INLINE_ARRAY_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[[^\]]*\]\s*:").unwrap());
#[derive(Debug, Clone, PartialEq)]
pub enum ScalarValue {
Null,
Bool(bool),
Int(i64),
Float(f64),
Str(String),
Missing,
Attachment,
}
pub fn needs_quote(s: &str) -> bool {
if s.is_empty() {
return true;
}
if matches!(s, "-" | "~" | "^" | "true" | "false") {
return true;
}
{
let b = s.as_bytes();
if b.len() >= 3 && b[0] == b'^' && b[1] == b'{' && b[b.len() - 1] == b'}' {
return true;
}
}
if JSON_NUMBER_RE.is_match(s) {
return true;
}
if NUMERIC_LIKE_RE.is_match(s) {
return true;
}
let bytes = s.as_bytes();
if bytes[0] == b' ' || bytes[bytes.len() - 1] == b' ' {
return true;
}
if bytes[0] == b'#' || bytes[0] == b'@' || bytes[0] == b'.' {
return true;
}
if INLINE_ARRAY_RE.is_match(s) {
return true;
}
for c in s.chars() {
let code = c as u32;
if c == '"'
|| c == '\\'
|| c == '|'
|| c == ','
|| code < 0x20
|| c == '\n'
|| c == '\r'
|| ((0x80..=0x9F).contains(&code)) || (code > 0x7F && matches!(code, 0xA0 | 0x1680 | 0x2028 | 0x2029 | 0x202F | 0x205F | 0x3000 | 0xFEFF))
|| (0x2000..=0x200A).contains(&code)
{
return true;
}
}
false
}
pub fn quote_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\u{0008}' => out.push_str("\\b"),
'\u{000C}' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out.push('"');
out
}
pub fn format_scalar(v: &serde_json::Value, delimiter: char) -> String {
match v {
serde_json::Value::Null => "-".to_string(),
serde_json::Value::Bool(b) => if *b { "true" } else { "false" }.to_string(),
serde_json::Value::Number(n) => format_number(n),
serde_json::Value::String(s) => {
if needs_quote(s) || (delimiter != '\0' && s.contains(delimiter)) {
quote_string(s)
} else {
s.clone()
}
}
_ => "-".to_string(),
}
}
pub fn format_number(n: &serde_json::Number) -> String {
if let Some(i) = n.as_i64() {
return i.to_string();
}
if let Some(u) = n.as_u64() {
return u.to_string();
}
if let Some(f) = n.as_f64() {
if f == 0.0 {
return "0".to_string();
}
let abs = f.abs();
if (1e-6..1e21).contains(&abs) {
let s = format!("{}", f);
if s.ends_with(".0") && f == f.trunc() {
return s[..s.len() - 2].to_string();
}
return s;
}
let s = format!("{:e}", f);
if let Some(pos) = s.find('e') {
let mantissa = s[..pos].trim_end_matches('0').trim_end_matches('.');
let exp_part = &s[pos + 1..];
let (sign, digits) = if let Some(rest) = exp_part.strip_prefix('-') {
("-", rest.trim_start_matches('0'))
} else if let Some(rest) = exp_part.strip_prefix('+') {
("+", rest.trim_start_matches('0'))
} else {
("+", exp_part.trim_start_matches('0'))
};
let digits = if digits.is_empty() { "0" } else { digits };
return format!("{}e{}{}", mantissa, sign, digits);
}
return s;
}
n.to_string()
}
pub fn is_bare_key(s: &str) -> bool {
BARE_KEY_RE.is_match(s)
}
pub fn format_key(s: &str) -> String {
if is_bare_key(s) {
s.to_string()
} else {
quote_string(s)
}
}
pub fn parse_scalar(s: &str, tabular_context: bool) -> Result<ScalarValue, String> {
if s.is_empty() {
return Ok(ScalarValue::Str(String::new()));
}
if s.starts_with('"') {
return parse_quoted_string(s).map(ScalarValue::Str);
}
if s == "-" {
return Ok(ScalarValue::Null);
}
if s == "~" {
if !tabular_context {
return Err("invalid_missing: ~ outside tabular row cell".into());
}
return Ok(ScalarValue::Missing);
}
if s == "^" {
if !tabular_context {
return Err("invalid_attachment_marker: ^ outside tabular row cell".into());
}
return Ok(ScalarValue::Attachment);
}
if s == "true" {
return Ok(ScalarValue::Bool(true));
}
if s == "false" {
return Ok(ScalarValue::Bool(false));
}
if JSON_NUMBER_RE.is_match(s) {
if let Ok(f) = s.parse::<f64>() {
if !s.contains('.')
&& !s.contains('e')
&& !s.contains('E')
&& f.abs() <= (1i64 << 53) as f64
{
return Ok(ScalarValue::Int(f as i64));
}
return Ok(ScalarValue::Float(f));
}
}
Ok(ScalarValue::Str(s.to_string()))
}
pub fn parse_quoted_string(s: &str) -> Result<String, String> {
let bytes = s.as_bytes();
if bytes.len() < 2 || bytes[0] != b'"' {
return Err("unterminated_quote".into());
}
let mut out = String::new();
let mut i = 1;
while i < bytes.len() {
if bytes[i] == b'"' {
if i + 1 != bytes.len() {
return Err("trailing_characters: after closing quote".into());
}
return Ok(out);
}
if bytes[i] == b'\\' {
if i + 1 >= bytes.len() {
return Err("unterminated_quote".into());
}
i += 1;
match bytes[i] {
b'"' => out.push('"'),
b'\\' => out.push('\\'),
b'/' => out.push('/'),
b'b' => out.push('\u{0008}'),
b'f' => out.push('\u{000C}'),
b'n' => out.push('\n'),
b'r' => out.push('\r'),
b't' => out.push('\t'),
b'u' => {
if i + 4 >= bytes.len() {
return Err("invalid_escape: incomplete unicode".into());
}
let hex = &s[i + 1..i + 5];
let code = u16::from_str_radix(hex, 16)
.map_err(|_| format!("invalid_escape: invalid unicode \\u{}", hex))?;
if (0xD800..=0xDBFF).contains(&code) {
if i + 10 >= bytes.len() || bytes[i + 5] != b'\\' || bytes[i + 6] != b'u' {
return Err("invalid_surrogate: isolated high surrogate".into());
}
let hex2 = &s[i + 7..i + 11];
let low = u16::from_str_radix(hex2, 16).map_err(|_| {
format!("invalid_surrogate: invalid low surrogate \\u{}", hex2)
})?;
if !(0xDC00..=0xDFFF).contains(&low) {
return Err("invalid_surrogate: expected low surrogate".into());
}
let combined =
0x10000 + (code as u32 - 0xD800) * 0x400 + (low as u32 - 0xDC00);
out.push(char::from_u32(combined).ok_or("invalid_surrogate")?);
i += 11;
continue;
}
if (0xDC00..=0xDFFF).contains(&code) {
return Err("invalid_surrogate: isolated low surrogate".into());
}
out.push(char::from_u32(code as u32).ok_or("invalid_escape")?);
i += 5;
continue;
}
c => return Err(format!("invalid_escape: unknown \\{}", c as char)),
}
i += 1;
continue;
}
if bytes[i] < 0x20 {
return Err(format!(
"invalid_escape: unescaped control U+{:04x}",
bytes[i]
));
}
let ch = s[i..].chars().next().unwrap();
out.push(ch);
i += ch.len_utf8();
}
Err("unterminated_quote".into())
}
pub fn split_respecting_quotes(s: &str, delim: char) -> Vec<String> {
let mut parts = Vec::new();
let mut current = String::new();
let mut in_quote = false;
let mut escaped = false;
for c in s.chars() {
if escaped {
current.push(c);
escaped = false;
continue;
}
if c == '\\' && in_quote {
current.push(c);
escaped = true;
continue;
}
if c == '"' {
in_quote = !in_quote;
current.push(c);
continue;
}
if c == delim && !in_quote {
parts.push(current.clone());
current.clear();
continue;
}
current.push(c);
}
parts.push(current);
parts
}
pub fn split_field_decl(s: &str) -> Result<Vec<String>, String> {
if s.len() < 2 || !s.starts_with('{') {
return Err(format!("invalid field declaration: {}", s));
}
let close = find_closing_brace(s).ok_or_else(|| format!("invalid field declaration: {}", s))?;
let inner = &s[1..close];
if inner.is_empty() {
return Ok(Vec::new());
}
let raw = split_respecting_quotes(inner, ',');
let mut fields = Vec::new();
let mut seen = std::collections::HashSet::new();
for f in raw {
let f = f.trim();
let name = if f.len() >= 2 && f.starts_with('"') && f.ends_with('"') {
parse_quoted_string(f)?
} else {
if !is_bare_key(f) {
return Err(format!("invalid field name: {}", f));
}
f.to_string()
};
if !seen.insert(name.clone()) {
return Err(format!("duplicate_field_name: {}", name));
}
fields.push(name);
}
Ok(fields)
}
pub fn find_closing_brace(s: &str) -> Option<usize> {
let mut in_quote = false;
let mut escaped = false;
for (i, c) in s.char_indices() {
if escaped {
escaped = false;
continue;
}
if c == '\\' && in_quote {
escaped = true;
continue;
}
if c == '"' {
in_quote = !in_quote;
continue;
}
if c == '}' && !in_quote {
return Some(i);
}
}
None
}