Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(prettier): Complete print_literal #7952

Merged
merged 10 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 21 additions & 110 deletions crates/oxc_prettier/src/format/js.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ use crate::{
format::{
print::{
array, arrow_function, assignment, binaryish, block, call_expression, class, function,
function_parameters, misc, module, object, property, string, template_literal, ternary,
function_parameters, literal, misc, module, object, property, template_literal,
ternary,
},
Format,
},
Expand Down Expand Up @@ -59,9 +60,10 @@ impl<'a> Format<'a> for Hashbang<'a> {
impl<'a> Format<'a> for Directive<'a> {
fn format(&self, p: &mut Prettier<'a>) -> Doc<'a> {
let mut parts = Vec::new_in(p.allocator);
parts.push(dynamic_text!(
parts.push(literal::print_string_from_not_quoted_raw_text(
p,
string::print_string(p, self.directive.as_str(), p.options.single_quote,)
self.directive.as_str(),
p.options.single_quote,
));
if let Some(semi) = p.semi() {
parts.push(semi);
Expand Down Expand Up @@ -905,64 +907,7 @@ impl<'a> Format<'a> for NullLiteral {

impl<'a> Format<'a> for NumericLiteral<'a> {
fn format(&self, p: &mut Prettier<'a>) -> Doc<'a> {
wrap!(p, self, NumericLiteral, {
// See https://github.com/prettier/prettier/blob/3.3.3/src/utils/print-number.js
// Perf: the regexes from prettier code above are ported to manual search for performance reasons.
let mut string = self.span.source_text(p.source_text).cow_to_ascii_lowercase();

// Remove unnecessary plus and zeroes from scientific notation.
if let Some((head, tail)) = string.split_once('e') {
let negative = if tail.starts_with('-') { "-" } else { "" };
let trimmed = tail.trim_start_matches(['+', '-']).trim_start_matches('0');
if trimmed.starts_with(|c: char| c.is_ascii_digit()) {
string = Cow::Owned(std::format!("{head}e{negative}{trimmed}"));
}
}

// Remove unnecessary scientific notation (1e0).
if let Some((head, tail)) = string.split_once('e') {
if tail.trim_start_matches(['+', '-']).trim_start_matches('0').is_empty() {
string = Cow::Owned(head.to_string());
}
}

// Make sure numbers always start with a digit.
if string.starts_with('.') {
string = Cow::Owned(std::format!("0{string}"));
}

// Remove extraneous trailing decimal zeroes.
if let Some((head, tail)) = string.split_once('.') {
if let Some((head_e, tail_e)) = tail.split_once('e') {
if !head_e.is_empty() {
let trimmed = head_e.trim_end_matches('0');
if trimmed.is_empty() {
string = Cow::Owned(std::format!("{head}.0e{tail_e}"));
} else {
string = Cow::Owned(std::format!("{head}.{trimmed}e{tail_e}"));
}
}
} else if !tail.is_empty() {
let trimmed = tail.trim_end_matches('0');
if trimmed.is_empty() {
string = Cow::Owned(std::format!("{head}.0"));
} else {
string = Cow::Owned(std::format!("{head}.{trimmed}"));
}
}
}

// Remove trailing dot.
if let Some((head, tail)) = string.split_once('.') {
if tail.is_empty() {
string = Cow::Owned(head.to_string());
} else if tail.starts_with('e') {
string = Cow::Owned(std::format!("{head}{tail}"));
}
}

dynamic_text!(p, &string)
})
literal::print_number(p, self.span.source_text(p.source_text))
}
}

Expand All @@ -977,22 +922,17 @@ impl<'a> Format<'a> for BigIntLiteral<'a> {

impl<'a> Format<'a> for RegExpLiteral<'a> {
fn format(&self, p: &mut Prettier<'a>) -> Doc<'a> {
let mut parts = Vec::new_in(p.allocator);
parts.push(text!("/"));
parts.push(dynamic_text!(p, self.regex.pattern.source_text(p.source_text).as_ref()));
parts.push(text!("/"));
parts.push(self.regex.flags.format(p));
array!(p, parts)
dynamic_text!(p, &self.regex.to_string())
}
}

impl<'a> Format<'a> for StringLiteral<'a> {
fn format(&self, p: &mut Prettier<'a>) -> Doc<'a> {
wrap!(p, self, StringLiteral, {
let raw = &p.source_text[(self.span.start + 1) as usize..(self.span.end - 1) as usize];
// TODO: implement `makeString` from prettier/src/utils/print-string.js
dynamic_text!(p, string::print_string(p, raw, p.options.single_quote))
})
literal::replace_end_of_line(
p,
literal::print_string(p, self.span.source_text(p.source_text), p.options.single_quote),
JoinSeparator::Literalline,
)
}
}

Expand Down Expand Up @@ -1214,9 +1154,10 @@ impl<'a> Format<'a> for PropertyKey<'a> {
match self {
PropertyKey::StaticIdentifier(ident) => {
if need_quote {
dynamic_text!(
literal::print_string_from_not_quoted_raw_text(
p,
string::print_string(p, &ident.name, p.options.single_quote)
&ident.name,
p.options.single_quote,
)
} else {
ident.format(p)
Expand All @@ -1233,17 +1174,19 @@ impl<'a> Format<'a> for PropertyKey<'a> {
{
dynamic_text!(p, literal.value.as_str())
} else {
dynamic_text!(
literal::print_string_from_not_quoted_raw_text(
p,
string::print_string(p, literal.value.as_str(), p.options.single_quote,)
literal.value.as_str(),
p.options.single_quote,
)
}
}
PropertyKey::NumericLiteral(literal) => {
if need_quote {
dynamic_text!(
literal::print_string_from_not_quoted_raw_text(
p,
string::print_string(p, &literal.raw_str(), p.options.single_quote)
&literal.raw_str(),
p.options.single_quote,
)
} else {
literal.format(p)
Expand Down Expand Up @@ -1723,35 +1666,3 @@ impl<'a> Format<'a> for AssignmentPattern<'a> {
})
}
}

impl<'a> Format<'a> for RegExpFlags {
fn format(&self, p: &mut Prettier<'a>) -> Doc<'a> {
let mut string = std::vec::Vec::with_capacity(self.iter().count());
if self.contains(Self::D) {
string.push('d');
}
if self.contains(Self::G) {
string.push('g');
}
if self.contains(Self::I) {
string.push('i');
}
if self.contains(Self::M) {
string.push('m');
}
if self.contains(Self::S) {
string.push('s');
}
if self.contains(Self::U) {
string.push('u');
}
if self.contains(Self::V) {
string.push('v');
}
if self.contains(Self::Y) {
string.push('y');
}
let sorted = string.iter().collect::<String>();
dynamic_text!(p, &sorted)
}
}
183 changes: 183 additions & 0 deletions crates/oxc_prettier/src/format/print/literal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
use std::borrow::Cow;

use cow_utils::CowUtils;
use oxc_allocator::String;
use oxc_span::Span;

use crate::{
dynamic_text,
ir::{Doc, JoinSeparator},
join, Prettier,
};

/// Print quoted string.
/// Quotes are automatically chosen based on the content of the string and option.
pub fn print_string<'a>(
p: &Prettier<'a>,
quoted_raw_text: &'a str,
prefer_single_quote: bool,
) -> Doc<'a> {
debug_assert!(
quoted_raw_text.starts_with('\'') && quoted_raw_text.ends_with('\'')
|| quoted_raw_text.starts_with('"') && quoted_raw_text.ends_with('"')
);

let original_quote = quoted_raw_text.chars().next().unwrap();
let not_quoted_raw_text = &quoted_raw_text[1..quoted_raw_text.len() - 1];

let enclosing_quote = get_preferred_quote(not_quoted_raw_text, prefer_single_quote);

// This keeps useless escape as-is
if original_quote == enclosing_quote {
return dynamic_text!(p, quoted_raw_text);
}

dynamic_text!(p, make_string(p, not_quoted_raw_text, enclosing_quote).into_bump_str())
}

// TODO: Can this be removed? It does not exist in Prettier
/// Print quoted string from not quoted text.
/// Mainly this is used to add quotes for object property keys.
pub fn print_string_from_not_quoted_raw_text<'a>(
p: &Prettier<'a>,
not_quoted_raw_text: &str,
prefer_single_quote: bool,
) -> Doc<'a> {
let enclosing_quote = get_preferred_quote(not_quoted_raw_text, prefer_single_quote);
dynamic_text!(p, make_string(p, not_quoted_raw_text, enclosing_quote).into_bump_str())
}

// See https://github.com/prettier/prettier/blob/3.3.3/src/utils/print-number.js
// Perf: the regexes from prettier code above are ported to manual search for performance reasons.
pub fn print_number<'a>(p: &Prettier<'a>, raw_text: &str) -> Doc<'a> {
let mut string = raw_text.cow_to_ascii_lowercase();

// Remove unnecessary plus and zeroes from scientific notation.
if let Some((head, tail)) = string.split_once('e') {
let negative = if tail.starts_with('-') { "-" } else { "" };
let trimmed = tail.trim_start_matches(['+', '-']).trim_start_matches('0');
if trimmed.starts_with(|c: char| c.is_ascii_digit()) {
string = Cow::Owned(std::format!("{head}e{negative}{trimmed}"));
}
}

// Remove unnecessary scientific notation (1e0).
if let Some((head, tail)) = string.split_once('e') {
if tail.trim_start_matches(['+', '-']).trim_start_matches('0').is_empty() {
string = Cow::Owned(head.to_string());
}
}

// Make sure numbers always start with a digit.
if string.starts_with('.') {
string = Cow::Owned(std::format!("0{string}"));
}

// Remove extraneous trailing decimal zeroes.
if let Some((head, tail)) = string.split_once('.') {
if let Some((head_e, tail_e)) = tail.split_once('e') {
if !head_e.is_empty() {
let trimmed = head_e.trim_end_matches('0');
if trimmed.is_empty() {
string = Cow::Owned(std::format!("{head}.0e{tail_e}"));
} else {
string = Cow::Owned(std::format!("{head}.{trimmed}e{tail_e}"));
}
}
} else if !tail.is_empty() {
let trimmed = tail.trim_end_matches('0');
if trimmed.is_empty() {
string = Cow::Owned(std::format!("{head}.0"));
} else {
string = Cow::Owned(std::format!("{head}.{trimmed}"));
}
}
}

// Remove trailing dot.
if let Some((head, tail)) = string.split_once('.') {
if tail.is_empty() {
string = Cow::Owned(head.to_string());
} else if tail.starts_with('e') {
string = Cow::Owned(std::format!("{head}{tail}"));
}
}

dynamic_text!(p, &string)
}

pub fn get_preferred_quote(not_quoted_raw_text: &str, prefer_single_quote: bool) -> char {
let (preferred_quote_char, alternate_quote_char) =
if prefer_single_quote { ('\'', '"') } else { ('"', '\'') };

let mut preferred_quote_count = 0;
let mut alternate_quote_count = 0;

for character in not_quoted_raw_text.chars() {
if character == preferred_quote_char {
preferred_quote_count += 1;
} else if character == alternate_quote_char {
alternate_quote_count += 1;
}
}

if preferred_quote_count > alternate_quote_count {
alternate_quote_char
} else {
preferred_quote_char
}
}

fn make_string<'a>(
p: &Prettier<'a>,
not_quoted_raw_text: &str,
enclosing_quote: char,
) -> String<'a> {
let other_quote = if enclosing_quote == '"' { '\'' } else { '"' };

let mut result = String::new_in(p.allocator);
result.push(enclosing_quote);

let mut chars = not_quoted_raw_text.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(&nc) = chars.peek() {
if nc == other_quote {
// Skip(remove) useless escape
chars.next();
result.push(nc);
} else {
result.push('\\');
if let Some(nc) = chars.next() {
result.push(nc);
}
}
} else {
result.push('\\');
}
} else if c == enclosing_quote {
result.push('\\');
result.push(c);
} else {
result.push(c);
}
}

result.push(enclosing_quote);
result
}

/// Handle line continuation.
/// This does not recursively handle the doc, expects single `Doc::Str`.
pub fn replace_end_of_line<'a>(
p: &Prettier<'a>,
doc: Doc<'a>,
replacement: JoinSeparator,
) -> Doc<'a> {
let Doc::Str(text) = doc else {
return doc;
};

let lines = text.split('\n').map(|line| dynamic_text!(p, line)).collect::<Vec<_>>();
join!(p, replacement, lines)
}
2 changes: 1 addition & 1 deletion crates/oxc_prettier/src/format/print/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ pub mod call_expression;
pub mod class;
pub mod function;
pub mod function_parameters;
pub mod literal;
pub mod misc;
pub mod module;
pub mod object;
pub mod property;
pub mod statement;
pub mod string;
pub mod template_literal;
pub mod ternary;
Loading
Loading