///|
priv struct DecimalSpelling {
mantissa_start : Int
exponent_marker : Int
is_integer : Bool
}
///|
fn decimal_spelling(
source : String,
start : Int,
end : Int,
) -> DecimalSpelling? {
let spelling = source.view(start_offset=start, end_offset=end)
let exponent_marker = lexmatch spelling with longest {
(re"^[0-9.]*[eE]", after=rest) => rest.start_offset() - 1
re"^[0-9.]*$" => end
_ => return None
}
let mantissa = source.view(start_offset=start, end_offset=exponent_marker)
let can_omit_leading_zero = mantissa =~ (re"^0\.[0-9.]", after=_)
Some({
mantissa_start: if can_omit_leading_zero {
start + 1
} else {
start
},
exponent_marker,
is_integer: mantissa =~ re"^[0-9]*$",
})
}
///|
fn integer_exponent_spelling(
source : String,
start : Int,
end : Int,
) -> String? {
let spelling = source.view(start_offset=start, end_offset=end)
// Leading-zero integers may be legacy octal.
guard !(spelling =~ (re"^0", after=_)) else { return None }
let trailing_zeros = lexmatch spelling {
(re"0+$" as zeros, before=_) => zeros.length()
_ => return None
}
let significant_end = end - trailing_zeros
let exponent = trailing_zeros.to_string()
let shortened_length = significant_end - start + 1 + exponent.length()
guard shortened_length < end - start else { return None }
let text = StringBuilder()
text.write_substring(source, start, significant_end - start)
text.write_string("e")
text.write_string(exponent)
Some(text.to_string())
}
///|
fn shortened_exponent_spelling(
source : String,
start : Int,
end : Int,
decimal : DecimalSpelling,
) -> String? {
let marker = decimal.exponent_marker
guard marker + 1 < end else { return None }
let exponent = source.view(start_offset=marker + 1, end_offset=end)
let (digits, has_minus) = lexmatch exponent with longest {
(re"^-", after=digits) => (digits, true)
(re"^\+", after=digits) => (digits, false)
_ => (exponent, false)
}
guard digits =~ re"^[0-9]+$" else { return None }
let (significant_start, exponent_is_zero) = lexmatch digits with longest {
(re"^0+", after=rest) =>
if rest.length() == 0 {
(end - 1, true)
} else {
(rest.start_offset(), false)
}
_ => (digits.start_offset(), false)
}
let needs_minus = has_minus && !exponent_is_zero
let mantissa_length = marker - decimal.mantissa_start
let exponent_length = end -
significant_start +
(if needs_minus { 1 } else { 0 })
guard mantissa_length + 1 + exponent_length < end - start else { return None }
let text = StringBuilder()
text.write_substring(source, decimal.mantissa_start, mantissa_length)
text.write_string("e")
if needs_minus {
text.write_string("-")
}
text.write_substring(source, significant_start, end - significant_start)
Some(text.to_string())
}
///|
/// Only rewrite decimal spellings whose numeric value is unchanged exactly.
fn Printer::number(self : Printer, start : Int, end : Int) -> Unit {
let source = self.program.source
guard decimal_spelling(source, start, end) is Some(decimal) else {
self.span(start, end, true)
return
}
guard decimal.exponent_marker != end else {
// BigInt and other bases were excluded by the decimal scan.
guard decimal.is_integer &&
integer_exponent_spelling(source, start, end) is Some(text) else {
return self.span(decimal.mantissa_start, end, true)
}
return self.text(text, true)
}
guard shortened_exponent_spelling(source, start, end, decimal) is Some(text) else {
return self.span(start, end, true)
}
self.text(text, true)
}
///|
priv struct QuoteCosts {
unescaped_length : Int
single_quotes : Int
double_quotes : Int
}
///|
fn quote_costs(source : String, start : Int, end : Int) -> QuoteCosts {
for index = start + 1, unescaped_length = end - start, single_quotes = 0, double_quotes = 0 {
guard index < end - 1 else {
break { unescaped_length, single_quotes, double_quotes, }
}
match quoted_string_piece(source, index, end) {
Unchanged(next) =>
continue next, unescaped_length, single_quotes, double_quotes
SingleQuote(next) =>
continue next, unescaped_length, single_quotes + 1, double_quotes
DoubleQuote(next) =>
continue next, unescaped_length, single_quotes, double_quotes + 1
EscapedSingleQuote(next) =>
continue next, unescaped_length - 1, single_quotes + 1, double_quotes
EscapedDoubleQuote(next) =>
continue next, unescaped_length - 1, single_quotes, double_quotes + 1
}
}
}
///|
priv enum QuotedStringPiece {
Unchanged(Int)
SingleQuote(Int)
DoubleQuote(Int)
EscapedSingleQuote(Int)
EscapedDoubleQuote(Int)
}
///|
fn quoted_string_piece(
source : String,
start : Int,
end : Int,
) -> QuotedStringPiece {
lexmatch source.view(start_offset=start, end_offset=end) with longest {
(re"^[^\\'\"]+", after=rest) => Unchanged(rest.start_offset())
(re"^\\'", after=rest) => EscapedSingleQuote(rest.start_offset())
(re"^\\\"", after=rest) => EscapedDoubleQuote(rest.start_offset())
(re"^'", after=rest) => SingleQuote(rest.start_offset())
(re"^\"", after=rest) => DoubleQuote(rest.start_offset())
(re"^\\[\u0000-\u{10FFFF}]", after=rest) => Unchanged(rest.start_offset())
// Regex character classes exclude isolated UTF-16 surrogates.
(re"^\\", after=_) => Unchanged(start + 2)
_ => Unchanged(start + 1)
}
}
///|
fn QuoteCosts::preferred_quote(self : QuoteCosts, original : UInt16) -> UInt16 {
guard self.single_quotes != self.double_quotes else { return original }
if self.single_quotes < self.double_quotes {
'\''
} else {
'"'
}
}
///|
fn QuoteCosts::quoted_length(self : QuoteCosts, quote : UInt16) -> Int {
self.unescaped_length +
(if quote == '\'' { self.single_quotes } else { self.double_quotes })
}
///|
/// Change quote escapes only. Other escapes and line continuations retain
/// their source spelling, including escapes in directive-like strings.
fn Printer::string(self : Printer, start : Int, end : Int) -> Unit {
let source = self.program.source
guard end > start + 1 else {
self.span(start, end, true)
return
}
let spelling = source.view(start_offset=start, end_offset=end)
guard spelling =~ (re"^['\"]", after=_) else {
// Property keys and module aliases also use StringLiteral arena nodes.
self.span(start, end, true)
return
}
let costs = quote_costs(source, start, end)
let quote = costs.preferred_quote(source[start])
guard costs.quoted_length(quote) < end - start else {
self.span(start, end, false)
return
}
self.text(quoted_text(source, start, end, quote), false)
}
///|
fn quoted_text(
source : String,
start : Int,
end : Int,
quote : UInt16,
) -> String {
let text = StringBuilder()
let delimiter = if quote == '\'' { "'" } else { "\"" }
text.write_string(delimiter)
let remaining = for index = start + 1, segment_start = start + 1 {
guard index < end - 1 else { break segment_start }
let (next_index, character, is_escaped) : (Int, UInt16, Bool) = match
quoted_string_piece(source, index, end) {
Unchanged(next) => continue next, segment_start
SingleQuote(next) => (next, '\'', false)
DoubleQuote(next) => (next, '"', false)
EscapedSingleQuote(next) => (next, '\'', true)
EscapedDoubleQuote(next) => (next, '"', true)
}
guard is_escaped || character == quote else {
continue next_index, segment_start
}
text.write_substring(source, segment_start, index - segment_start)
if character == quote {
text.write_string("\\")
}
text.write_substring(source, next_index - 1, 1)
continue next_index, next_index
}
// Copy contiguous source spans so UTF-16 surrogate pairs are never split.
text.write_substring(source, remaining, end - 1 - remaining)
text.write_string(delimiter)
text.to_string()
}