1use crate::lex::SyntaxKind;
36use crate::scalar::{places_to_decimal, signed_digits_to_decimal, CoreScalarType, ScalarValue};
37use crate::yaml::{Document, SyntaxNode};
38use rowan::ast::AstNode;
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct Rewrite {
43 pub before: String,
45 pub after: String,
47}
48
49pub fn to_yaml_1_2(doc: &Document) -> Vec<Rewrite> {
75 let mut rewrites = Vec::new();
76 rewrite_in(doc.syntax(), &mut rewrites);
77 rewrites
78}
79
80fn rewrite_in(node: &SyntaxNode, rewrites: &mut Vec<Rewrite>) {
81 for child in node.children() {
82 if child.kind() == SyntaxKind::SCALAR {
83 if let Some((after, kind)) = replacement_for(&child) {
84 let before = child.text().to_string();
85 let count = child.children_with_tokens().count();
86 crate::nodes::replace_children(
87 &child,
88 0..count,
89 vec![crate::nodes::fresh_token(kind, &after).into()],
90 );
91 rewrites.push(Rewrite { before, after });
92 }
93 continue;
94 }
95 rewrite_in(&child, rewrites);
96 }
97}
98
99fn replacement_for(scalar: &SyntaxNode) -> Option<(String, SyntaxKind)> {
102 let mut tokens = scalar.children_with_tokens();
105 let token = tokens.next()?.into_token()?;
106 if tokens.next().is_some() {
107 return None;
108 }
109 if !matches!(
110 token.kind(),
111 SyntaxKind::INT | SyntaxKind::FLOAT | SyntaxKind::STRING | SyntaxKind::BOOL
112 ) {
113 return None;
114 }
115
116 let text = token.text();
117 if text.starts_with(['"', '\'']) {
119 return None;
120 }
121
122 if scalar
125 .parent()
126 .is_some_and(|p| p.kind() == SyntaxKind::TAGGED_NODE)
127 {
128 return None;
129 }
130
131 if let Some(number) = yaml_1_1_number(text) {
132 return Some(number);
133 }
134
135 match text {
136 "yes" | "Yes" | "YES" | "on" | "On" | "ON" => Some(("true".into(), SyntaxKind::BOOL)),
137 "no" | "No" | "NO" | "off" | "Off" | "OFF" => Some(("false".into(), SyntaxKind::BOOL)),
138 _ if yaml_1_1_string(text) => Some((format!("'{text}'"), SyntaxKind::STRING)),
139 _ => None,
140 }
141}
142
143pub(crate) fn yaml_1_1_string(text: &str) -> bool {
156 match ScalarValue::classify_plain(text) {
157 CoreScalarType::Integer => {
158 let body = text.strip_prefix(['+', '-']).unwrap_or(text);
159 body.starts_with("0o") || ScalarValue::is_leading_zero_decimal(text)
160 }
161 CoreScalarType::Float => text
162 .find(['e', 'E'])
163 .is_some_and(|i| !text[..i].contains('.') || !text[i + 1..].starts_with(['+', '-'])),
164 _ => false,
165 }
166}
167
168pub(crate) fn yaml_1_1_number(text: &str) -> Option<(String, SyntaxKind)> {
171 let (sign, body) = match text.strip_prefix(['+', '-']) {
172 Some(body) => (&text[..1], body),
173 None => ("", text),
174 };
175 let negative = sign == "-";
176 let decimal = |digits: &str, radix: u32| {
177 (
178 signed_digits_to_decimal(digits, radix, negative),
179 SyntaxKind::INT,
180 )
181 };
182
183 if let Some(digits) = body.strip_prefix("0b") {
184 return radix_digits(digits, 2).map(|d| decimal(&d, 2));
185 }
186 if let Some(written) = body.strip_prefix("0x") {
187 let digits = radix_digits(written, 16)?;
188 if !sign.is_empty() {
190 return Some(decimal(&digits, 16));
191 }
192 return written
193 .contains('_')
194 .then(|| (format!("0x{digits}"), SyntaxKind::INT));
195 }
196 if body.contains(':') {
197 return sexagesimal(sign, body);
198 }
199 if let Some(digits) = body.strip_prefix('0').and_then(|d| radix_digits(d, 8)) {
202 if negative {
203 return Some(decimal(&digits, 8));
204 }
205 let digits = digits.trim_start_matches('0');
206 let digits = if digits.is_empty() { "0" } else { digits };
207 return Some((format!("0o{digits}"), SyntaxKind::INT));
208 }
209
210 if !body.contains('_') || !body.starts_with(|c: char| c.is_ascii_digit()) {
211 return None;
212 }
213 let (mantissa, exponent) = match body.find(['e', 'E']) {
214 Some(i) => body.split_at(i),
215 None => (body, ""),
216 };
217 let is_digits = |s: &str| s.bytes().all(|b| b.is_ascii_digit() || b == b'_');
218 let kind = match mantissa.split_once('.') {
219 None if exponent.is_empty() && is_digits(mantissa) && !mantissa.starts_with('0') => {
220 SyntaxKind::INT
221 }
222 Some((int, frac)) if is_digits(int) && is_digits(frac) => SyntaxKind::FLOAT,
223 _ => return None,
224 };
225 if !exponent.is_empty() {
226 let digits = exponent[1..].strip_prefix(['+', '-'])?;
227 if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
228 return None;
229 }
230 }
231 Some((format!("{sign}{}", body.replace('_', "")), kind))
232}
233
234fn radix_digits(digits: &str, radix: u32) -> Option<String> {
237 if !digits.chars().all(|c| c == '_' || c.is_digit(radix)) {
238 return None;
239 }
240 let digits = digits.replace('_', "");
241 (!digits.is_empty()).then_some(digits)
242}
243
244fn sexagesimal(sign: &str, body: &str) -> Option<(String, SyntaxKind)> {
247 let mut parts = body.split(':');
248 let first = parts.next()?;
249 let mut rest: Vec<&str> = parts.collect();
250 let last = rest.pop()?;
251 let (last, frac) = match last.split_once('.') {
252 Some((last, frac)) => (last, Some(frac)),
253 None => (last, None),
254 };
255 if !first.starts_with(|c: char| c.is_ascii_digit())
257 || (frac.is_none() && first.starts_with('0'))
258 {
259 return None;
260 }
261 let mut places: Vec<(u32, u32)> = radix_digits(first, 10)?
262 .chars()
263 .map(|c| (10, c.to_digit(10).expect("checked by radix_digits")))
264 .collect();
265 for part in rest.into_iter().chain([last]) {
266 let digit = match part.as_bytes() {
267 [d @ b'0'..=b'9'] => d - b'0',
268 [t @ b'0'..=b'5', d @ b'0'..=b'9'] => (t - b'0') * 10 + (d - b'0'),
269 _ => return None,
270 };
271 places.push((60, digit.into()));
272 }
273 let value = places_to_decimal(places);
274 match frac {
275 None => Some((format!("{sign}{value}"), SyntaxKind::INT)),
276 Some(frac) if frac.bytes().all(|b| b.is_ascii_digit() || b == b'_') => {
277 let frac = frac.replace('_', "");
278 let frac = if frac.is_empty() { "0" } else { &frac };
279 Some((format!("{sign}{value}.{frac}"), SyntaxKind::FLOAT))
280 }
281 _ => None,
282 }
283}