feat(rome_formatter): normalize line terminators in template literals
MODIFY
crates/rome_formatter/src/format_element.rs
crates/rome_formatter/src/format_element.rs
@@ -4,6 +4,7 @@ use rslint_parser::{AstNode, SyntaxNode};
use crate::format_elements;
use crate::intersperse::{Intersperse, IntersperseFn};
+use std::borrow::Cow;
use std::ops::Deref;
type Content = Box<FormatElement>;
@@ -922,14 +923,7 @@ impl Token {
/// Create a token from a dynamic string and a range of the input source
pub(crate) fn new_dynamic(text: String, source: TextRange) -> Self {
- cfg_if::cfg_if! {
- if #[cfg(debug_assertions)] {
- for line_break in text.matches(&['\r', LINE_SEPARATOR, PARAGRAPH_SEPARATOR]) {
- panic!("The content '{}' contains an unsupported {:?} line terminator character but string tokens must only use line feeds '\\n' as line separator. Use '\\n' instead of '\\r' and '\\r\\n' to insert a line break in strings.", text, line_break);
- }
- }
- }
-
+ debug_assert!(!text.contains('\r'), "The content '{}' contains an unsupported '\\r' line terminator character but string tokens must only use line feeds '\\n' as line separator. Use '\\n' instead of '\\r' and '\\r\\n' to insert a line break in strings.", text);
Self::Dynamic { text, source }
}
@@ -1000,18 +994,40 @@ impl<'a, L: Language> From<&'a SyntaxToken<L>> for Token {
const LINE_SEPARATOR: char = '\u{2028}';
const PARAGRAPH_SEPARATOR: char = '\u{2029}';
+pub(crate) const LINE_TERMINATORS: [char; 3] = ['\r', LINE_SEPARATOR, PARAGRAPH_SEPARATOR];
+
+/// Replace the line terminators matching the provided list with "\n"
+/// since its the only line break type supported by the printer
+pub(crate) fn normalize_newlines<const N: usize>(text: &str, terminators: [char; N]) -> Cow<str> {
+ let mut result = String::new();
+ let mut last_end = 0;
+
+ for (start, part) in text.match_indices(terminators) {
+ result.push_str(&text[last_end..start]);
+ result.push('\n');
-/// Normalize all line terminators in the text to "\n" since
-/// its the only line break type supported by the printer
-pub(crate) fn normalize_newlines(text: &str) -> String {
- text.replace("\r\n", "\n")
- .replace(&['\r', LINE_SEPARATOR, PARAGRAPH_SEPARATOR], "\n")
+ last_end = start + part.len();
+ // If the current character is \r and the
+ // next is \n, skip over the entire sequence
+ if part == "\r" && text[last_end..].starts_with('\n') {
+ last_end += 1;
+ }
+ }
+
+ // If the result is empty no line terminators were matched,
+ // return the entire input text without allocating a new String
+ if result.is_empty() {
+ Cow::Borrowed(text)
+ } else {
+ result.push_str(&text[last_end..text.len()]);
+ Cow::Owned(result)
+ }
}
impl<L: Language> From<SyntaxTriviaPieceComments<L>> for Token {
fn from(trivia: SyntaxTriviaPieceComments<L>) -> Self {
Self::new_dynamic(
- normalize_newlines(trivia.text().trim()),
+ normalize_newlines(trivia.text().trim(), LINE_TERMINATORS).into_owned(),
trivia.text_range(),
)
}

MODIFY
crates/rome_formatter/src/formatter.rs
crates/rome_formatter/src/formatter.rs
@@ -6,7 +6,7 @@ use std::collections::HashSet;
use crate::formatter_traits::FormatTokenAndNode;
use crate::{
concat_elements, empty_element, empty_line,
- format_element::{normalize_newlines, Token},
+ format_element::{normalize_newlines, Token, LINE_TERMINATORS},
format_elements, hard_line_break, if_group_breaks, if_group_fits_on_single_line,
join_elements_hard_line, line_suffix, soft_line_break_or_space, space_token, FormatElement,
FormatOptions, FormatResult, ToFormatElement,
@@ -381,7 +381,7 @@ impl Formatter {
// Print the full (not trimmed) text of the token
FormatElement::from(Token::new_dynamic(
- normalize_newlines(syntax_token.text()),
+ normalize_newlines(syntax_token.text(), LINE_TERMINATORS).into_owned(),
syntax_token.text_range(),
))
}

MODIFY
crates/rome_formatter/src/ts/expressions/template_chunk_element.rs
crates/rome_formatter/src/ts/expressions/template_chunk_element.rs
-use crate::{FormatElement, FormatResult, Formatter, ToFormatElement};
+use crate::{
+ format_element::{normalize_newlines, Token},
+ FormatElement, FormatResult, Formatter, ToFormatElement,
+};
use rslint_parser::ast::JsTemplateChunkElement;
impl ToFormatElement for JsTemplateChunkElement {
fn to_format_element(&self, formatter: &Formatter) -> FormatResult<FormatElement> {
- formatter.format_token(&self.template_chunk_token()?)
+ // Per https://tc39.es/ecma262/multipage/ecmascript-language-lexical-grammar.html#sec-static-semantics-trv:
+ // In template literals, the '\r' and '\r\n' line terminators are normalized to '\n'
+ let chunk = self.template_chunk_token()?;
+ formatter.format_replaced(
+ &chunk,
+ FormatElement::from(Token::new_dynamic(
+ normalize_newlines(chunk.text_trimmed(), ['\r']).into_owned(),
+ chunk.text_trimmed_range(),
+ )),
+ )
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::{format, FormatOptions};
+ use rslint_parser::parse_script;
+
+ #[test]
+ fn template_line_breaks() {
+ const CODE: &str = "
+ const CR = `\r`;
+ const LF = `\n`;
+ const CR_LF = `\r\n`;
+ const LS = `\u{2028}`;
+ const PS = `\u{2029}`;
+ ";
+
+ let tree = parse_script(CODE, 0).syntax();
+ let result = format(FormatOptions::default(), &tree).unwrap();
+ assert!(!result.into_code().contains('\r'));
}
}

feat(rome_formatter): normalize line terminators in template literals (#2036)