use crate::lex::SyntaxKind;
use crate::scalar::{places_to_decimal, signed_digits_to_decimal, CoreScalarType, ScalarValue};
use crate::yaml::{Document, SyntaxNode};
use rowan::ast::AstNode;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Rewrite {
pub before: String,
pub after: String,
}
pub fn to_yaml_1_2(doc: &Document) -> Vec<Rewrite> {
let mut rewrites = Vec::new();
rewrite_in(doc.syntax(), &mut rewrites);
rewrites
}
fn rewrite_in(node: &SyntaxNode, rewrites: &mut Vec<Rewrite>) {
for child in node.children() {
if child.kind() == SyntaxKind::SCALAR {
if let Some((after, kind)) = replacement_for(&child) {
let before = child.text().to_string();
let count = child.children_with_tokens().count();
crate::nodes::replace_children(
&child,
0..count,
vec![crate::nodes::fresh_token(kind, &after).into()],
);
rewrites.push(Rewrite { before, after });
}
continue;
}
rewrite_in(&child, rewrites);
}
}
fn replacement_for(scalar: &SyntaxNode) -> Option<(String, SyntaxKind)> {
let mut tokens = scalar.children_with_tokens();
let token = tokens.next()?.into_token()?;
if tokens.next().is_some() {
return None;
}
if !matches!(
token.kind(),
SyntaxKind::INT | SyntaxKind::FLOAT | SyntaxKind::STRING | SyntaxKind::BOOL
) {
return None;
}
let text = token.text();
if text.starts_with(['"', '\'']) {
return None;
}
if scalar
.parent()
.is_some_and(|p| p.kind() == SyntaxKind::TAGGED_NODE)
{
return None;
}
if let Some(number) = yaml_1_1_number(text) {
return Some(number);
}
match text {
"yes" | "Yes" | "YES" | "on" | "On" | "ON" => Some(("true".into(), SyntaxKind::BOOL)),
"no" | "No" | "NO" | "off" | "Off" | "OFF" => Some(("false".into(), SyntaxKind::BOOL)),
_ if yaml_1_1_string(text) => Some((format!("'{text}'"), SyntaxKind::STRING)),
_ => None,
}
}
pub(crate) fn yaml_1_1_string(text: &str) -> bool {
match ScalarValue::classify_plain(text) {
CoreScalarType::Integer => {
let body = text.strip_prefix(['+', '-']).unwrap_or(text);
body.starts_with("0o") || ScalarValue::is_leading_zero_decimal(text)
}
CoreScalarType::Float => text
.find(['e', 'E'])
.is_some_and(|i| !text[..i].contains('.') || !text[i + 1..].starts_with(['+', '-'])),
_ => false,
}
}
pub(crate) fn yaml_1_1_number(text: &str) -> Option<(String, SyntaxKind)> {
let (sign, body) = match text.strip_prefix(['+', '-']) {
Some(body) => (&text[..1], body),
None => ("", text),
};
let negative = sign == "-";
let decimal = |digits: &str, radix: u32| {
(
signed_digits_to_decimal(digits, radix, negative),
SyntaxKind::INT,
)
};
if let Some(digits) = body.strip_prefix("0b") {
return radix_digits(digits, 2).map(|d| decimal(&d, 2));
}
if let Some(written) = body.strip_prefix("0x") {
let digits = radix_digits(written, 16)?;
if !sign.is_empty() {
return Some(decimal(&digits, 16));
}
return written
.contains('_')
.then(|| (format!("0x{digits}"), SyntaxKind::INT));
}
if body.contains(':') {
return sexagesimal(sign, body);
}
if let Some(digits) = body.strip_prefix('0').and_then(|d| radix_digits(d, 8)) {
if negative {
return Some(decimal(&digits, 8));
}
let digits = digits.trim_start_matches('0');
let digits = if digits.is_empty() { "0" } else { digits };
return Some((format!("0o{digits}"), SyntaxKind::INT));
}
if !body.contains('_') || !body.starts_with(|c: char| c.is_ascii_digit()) {
return None;
}
let (mantissa, exponent) = match body.find(['e', 'E']) {
Some(i) => body.split_at(i),
None => (body, ""),
};
let is_digits = |s: &str| s.bytes().all(|b| b.is_ascii_digit() || b == b'_');
let kind = match mantissa.split_once('.') {
None if exponent.is_empty() && is_digits(mantissa) && !mantissa.starts_with('0') => {
SyntaxKind::INT
}
Some((int, frac)) if is_digits(int) && is_digits(frac) => SyntaxKind::FLOAT,
_ => return None,
};
if !exponent.is_empty() {
let digits = exponent[1..].strip_prefix(['+', '-'])?;
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
}
Some((format!("{sign}{}", body.replace('_', "")), kind))
}
fn radix_digits(digits: &str, radix: u32) -> Option<String> {
if !digits.chars().all(|c| c == '_' || c.is_digit(radix)) {
return None;
}
let digits = digits.replace('_', "");
(!digits.is_empty()).then_some(digits)
}
fn sexagesimal(sign: &str, body: &str) -> Option<(String, SyntaxKind)> {
let mut parts = body.split(':');
let first = parts.next()?;
let mut rest: Vec<&str> = parts.collect();
let last = rest.pop()?;
let (last, frac) = match last.split_once('.') {
Some((last, frac)) => (last, Some(frac)),
None => (last, None),
};
if !first.starts_with(|c: char| c.is_ascii_digit())
|| (frac.is_none() && first.starts_with('0'))
{
return None;
}
let mut places: Vec<(u32, u32)> = radix_digits(first, 10)?
.chars()
.map(|c| (10, c.to_digit(10).expect("checked by radix_digits")))
.collect();
for part in rest.into_iter().chain([last]) {
let digit = match part.as_bytes() {
[d @ b'0'..=b'9'] => d - b'0',
[t @ b'0'..=b'5', d @ b'0'..=b'9'] => (t - b'0') * 10 + (d - b'0'),
_ => return None,
};
places.push((60, digit.into()));
}
let value = places_to_decimal(places);
match frac {
None => Some((format!("{sign}{value}"), SyntaxKind::INT)),
Some(frac) if frac.bytes().all(|b| b.is_ascii_digit() || b == b'_') => {
let frac = frac.replace('_', "");
let frac = if frac.is_empty() { "0" } else { &frac };
Some((format!("{sign}{value}.{frac}"), SyntaxKind::FLOAT))
}
_ => None,
}
}