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
23impl EmitError {
24 pub fn code(&self) -> &'static str {
26 match self {
27 Self::IntegerTooLarge(_) => "edit.integer-too-large",
28 Self::DuplicateKey(_) => "edit.duplicate-key",
29 Self::KeyTooLong(_) => "edit.key-too-long",
30 }
31 }
32}
33
34const MAX_KEY_CHARS: usize = 1024;
36
37pub fn emit_value(value: &Value, indent: usize) -> Result<String, EmitError> {
43 check(value)?;
44 Ok(emit(value, indent))
45}
46
47pub fn rewrite_block(values: &[(String, Value)]) -> Result<String, EmitError> {
50 let mut seen = std::collections::HashSet::new();
51 let mut out = String::new();
52 for (key, value) in values {
53 if !seen.insert(key.as_str()) {
54 return Err(EmitError::DuplicateKey(key.clone()));
55 }
56 check_key(key)?;
57 check(value)?;
58 push_pair(&mut out, key, value, 0);
59 out.push('\n');
60 }
61 Ok(out)
62}
63
64fn check(value: &Value) -> Result<(), EmitError> {
65 match value {
66 Value::Number(n) => check_number(n),
67 Value::Array(values) => values.iter().try_for_each(check),
68 Value::Object(map) => map.iter().try_for_each(|(key, value)| {
69 check_key(key)?;
70 check(value)
71 }),
72 _ => Ok(()),
73 }
74}
75
76fn check_key(key: &str) -> Result<(), EmitError> {
77 let written = scalar(key).chars().count();
78 if written > MAX_KEY_CHARS {
79 Err(EmitError::KeyTooLong(written))
80 } else {
81 Ok(())
82 }
83}
84
85fn check_number(n: &Number) -> Result<(), EmitError> {
88 let too_large = n
89 .as_i64()
90 .is_some_and(|v| v.unsigned_abs() > MAX_SAFE_INTEGER)
91 || n.as_u64().is_some_and(|v| v > MAX_SAFE_INTEGER);
92 if too_large {
93 Err(EmitError::IntegerTooLarge(n.to_string()))
94 } else {
95 Ok(())
96 }
97}
98
99fn number(n: &Number) -> String {
103 match n.as_f64() {
104 Some(v) if n.is_f64() && v.fract() == 0.0 && v.abs() <= MAX_SAFE_INTEGER as f64 => {
105 (v as i64).to_string()
106 }
107 _ => n.to_string(),
108 }
109}
110
111fn push_pair(out: &mut String, key: &str, value: &Value, column: usize) {
112 out.push_str(&scalar(key));
113 out.push(':');
114 let rendered = emit(value, column);
115 if !rendered.starts_with('\n') {
116 out.push(' ');
117 }
118 out.push_str(&rendered);
119}
120
121fn emit(value: &Value, indent: usize) -> String {
122 let child = indent + 2;
123 match value {
124 Value::Null => "null".into(),
125 Value::Bool(v) => v.to_string(),
126 Value::Number(v) => number(v),
127 Value::String(v) => scalar(v),
128 Value::Array(values) if values.is_empty() => "[]".into(),
129 Value::Object(values) if values.is_empty() => "{}".into(),
130 Value::Array(values) => {
131 let mut out = String::new();
132 for item in values {
133 out.push('\n');
134 out.push_str(&" ".repeat(child));
135 out.push('-');
136 let rendered = emit(item, child);
137 match rendered.strip_prefix('\n') {
138 Some(block) => {
140 out.push(' ');
141 out.push_str(&block[child + 2..]);
142 }
143 None => {
144 out.push(' ');
145 out.push_str(&rendered);
146 }
147 }
148 }
149 out
150 }
151 Value::Object(values) => {
152 let mut out = String::new();
153 for (key, value) in values {
154 out.push('\n');
155 out.push_str(&" ".repeat(child));
156 push_pair(&mut out, key, value, child);
157 }
158 out
159 }
160 }
161}
162
163fn scalar(s: &str) -> String {
165 if is_plain_safe(s) {
166 return s.to_owned();
167 }
168 let mut out = String::with_capacity(s.len() + 2);
169 out.push('"');
170 for c in s.chars() {
171 match c {
172 '"' => out.push_str("\\\""),
173 '\\' => out.push_str("\\\\"),
174 '\n' => out.push_str("\\n"),
175 '\r' => out.push_str("\\r"),
176 '\t' => out.push_str("\\t"),
177 c if !is_printable(c) => {
178 let n = c as u32;
179 if n <= 0xff {
180 out.push_str(&format!("\\x{n:02x}"));
181 } else {
182 out.push_str(&format!("\\u{n:04x}"));
183 }
184 }
185 c => out.push(c),
186 }
187 }
188 out.push('"');
189 out
190}
191
192fn is_printable(c: char) -> bool {
195 matches!(c, ' '..='~' | '\u{a0}'..='\u{d7ff}' | '\u{e000}'..='\u{fffd}' | '\u{10000}'..='\u{10ffff}')
196 && !matches!(c, '\u{2028}' | '\u{2029}' | '\u{feff}')
197}
198
199fn is_plain_safe(s: &str) -> bool {
202 let mut chars = s.chars();
203 let Some(first) = chars.next() else {
204 return false;
205 };
206 (first.is_ascii_alphabetic() || first == '_')
207 && chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '/' | '-'))
208 && !matches!(
209 s.to_ascii_lowercase().as_str(),
210 "true"
211 | "false"
212 | "yes"
213 | "no"
214 | "on"
215 | "off"
216 | "y"
217 | "n"
218 | "null"
219 | "inf"
220 | "nan"
221 | "infinity"
222 )
223}