1use serde_json::Number;
9
10use crate::ordered::{MAX_SAFE_INTEGER, OrderedValue as Value};
11
12#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
14pub enum EmitError {
15 #[error("integer {0} is beyond ±(2^53 − 1) and cannot cross to JavaScript exactly")]
16 IntegerTooLarge(String),
17 #[error("duplicate frontmatter key `{0}`")]
18 DuplicateKey(String),
19 #[error("mapping key of {0} characters is over the 1024 YAML allows for an implicit key")]
20 KeyTooLong(usize),
21}
22
23const MAX_KEY_CHARS: usize = 1024;
25
26pub fn emit_value(value: &Value, indent: usize) -> Result<String, EmitError> {
32 check(value)?;
33 Ok(emit(value, indent))
34}
35
36pub fn rewrite_block(values: &[(String, Value)]) -> Result<String, EmitError> {
39 let mut seen = std::collections::HashSet::new();
40 let mut out = String::new();
41 for (key, value) in values {
42 if !seen.insert(key.as_str()) {
43 return Err(EmitError::DuplicateKey(key.clone()));
44 }
45 check_key(key)?;
46 check(value)?;
47 push_pair(&mut out, key, value, 0);
48 out.push('\n');
49 }
50 Ok(out)
51}
52
53fn check(value: &Value) -> Result<(), EmitError> {
54 match value {
55 Value::Number(n) => check_number(n),
56 Value::Array(values) => values.iter().try_for_each(check),
57 Value::Object(map) => map.iter().try_for_each(|(key, value)| {
58 check_key(key)?;
59 check(value)
60 }),
61 _ => Ok(()),
62 }
63}
64
65fn check_key(key: &str) -> Result<(), EmitError> {
66 let written = scalar(key).chars().count();
67 if written > MAX_KEY_CHARS {
68 Err(EmitError::KeyTooLong(written))
69 } else {
70 Ok(())
71 }
72}
73
74fn check_number(n: &Number) -> Result<(), EmitError> {
77 let too_large = n
78 .as_i64()
79 .is_some_and(|v| v.unsigned_abs() > MAX_SAFE_INTEGER)
80 || n.as_u64().is_some_and(|v| v > MAX_SAFE_INTEGER);
81 if too_large {
82 Err(EmitError::IntegerTooLarge(n.to_string()))
83 } else {
84 Ok(())
85 }
86}
87
88fn number(n: &Number) -> String {
92 match n.as_f64() {
93 Some(v) if n.is_f64() && v.fract() == 0.0 && v.abs() <= MAX_SAFE_INTEGER as f64 => {
94 (v as i64).to_string()
95 }
96 _ => n.to_string(),
97 }
98}
99
100fn push_pair(out: &mut String, key: &str, value: &Value, column: usize) {
101 out.push_str(&scalar(key));
102 out.push(':');
103 let rendered = emit(value, column);
104 if !rendered.starts_with('\n') {
105 out.push(' ');
106 }
107 out.push_str(&rendered);
108}
109
110fn emit(value: &Value, indent: usize) -> String {
111 let child = indent + 2;
112 match value {
113 Value::Null => "null".into(),
114 Value::Bool(v) => v.to_string(),
115 Value::Number(v) => number(v),
116 Value::String(v) => scalar(v),
117 Value::Array(values) if values.is_empty() => "[]".into(),
118 Value::Object(values) if values.is_empty() => "{}".into(),
119 Value::Array(values) => {
120 let mut out = String::new();
121 for item in values {
122 out.push('\n');
123 out.push_str(&" ".repeat(child));
124 out.push('-');
125 let rendered = emit(item, child);
126 match rendered.strip_prefix('\n') {
127 Some(block) => {
129 out.push(' ');
130 out.push_str(&block[child + 2..]);
131 }
132 None => {
133 out.push(' ');
134 out.push_str(&rendered);
135 }
136 }
137 }
138 out
139 }
140 Value::Object(values) => {
141 let mut out = String::new();
142 for (key, value) in values {
143 out.push('\n');
144 out.push_str(&" ".repeat(child));
145 push_pair(&mut out, key, value, child);
146 }
147 out
148 }
149 }
150}
151
152fn scalar(s: &str) -> String {
154 if is_plain_safe(s) {
155 return s.to_owned();
156 }
157 let mut out = String::with_capacity(s.len() + 2);
158 out.push('"');
159 for c in s.chars() {
160 match c {
161 '"' => out.push_str("\\\""),
162 '\\' => out.push_str("\\\\"),
163 '\n' => out.push_str("\\n"),
164 '\r' => out.push_str("\\r"),
165 '\t' => out.push_str("\\t"),
166 c if !is_printable(c) => {
167 let n = c as u32;
168 if n <= 0xff {
169 out.push_str(&format!("\\x{n:02x}"));
170 } else {
171 out.push_str(&format!("\\u{n:04x}"));
172 }
173 }
174 c => out.push(c),
175 }
176 }
177 out.push('"');
178 out
179}
180
181fn is_printable(c: char) -> bool {
184 matches!(c, ' '..='~' | '\u{a0}'..='\u{d7ff}' | '\u{e000}'..='\u{fffd}' | '\u{10000}'..='\u{10ffff}')
185 && !matches!(c, '\u{2028}' | '\u{2029}' | '\u{feff}')
186}
187
188fn is_plain_safe(s: &str) -> bool {
191 let mut chars = s.chars();
192 let Some(first) = chars.next() else {
193 return false;
194 };
195 (first.is_ascii_alphabetic() || first == '_')
196 && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '/' | '-'))
197 && !matches!(
198 s.to_ascii_lowercase().as_str(),
199 "true"
200 | "false"
201 | "yes"
202 | "no"
203 | "on"
204 | "off"
205 | "y"
206 | "n"
207 | "null"
208 | "inf"
209 | "nan"
210 | "infinity"
211 )
212}