Add WhitespaceUtils (#13923)

This commit is contained in:
fisker Cheung authored and GitHub committed 2023-02-24 15:41:20 +08:00
1 parent 3662619f6f
commit 595a2beebc
8 files changed
+298 -69

No files matched your search

+3 -1
View File
@@ -1,3 +1,5 @@
import htmlWhitespaceUtils from "../utils/html-whitespace-utils.js";
function clean(ast, newNode /*, parent*/) {
// (Glimmer/HTML) ignore TextNode
if (ast.type === "TextNode") {
@@ -5,7 +7,7 @@ function clean(ast, newNode /*, parent*/) {
if (!trimmed) {
return null;
}
newNode.chars = trimmed.replaceAll(/[\t\n\f\r ]+/g, " ");
newNode.chars = htmlWhitespaceUtils.split(trimmed).join(" ");
}
// `class` is reformatted
+20 -22
View File
@@ -13,6 +13,7 @@ import { replaceEndOfLine } from "../document/utils.js";
import getPreferredQuote from "../utils/get-preferred-quote.js";
import isNonEmptyArray from "../utils/is-non-empty-array.js";
import UnexpectedNodeError from "../utils/unexpected-node-error.js";
import htmlWhitespaceUtils from "../utils/html-whitespace-utils.js";
import { locStart, locEnd } from "./loc.js";
import clean from "./clean.js";
import { hasPrettierIgnore, isVoidElement, isWhitespaceNode } from "./utils.js";
@@ -199,15 +200,10 @@ function print(path, options, print) {
return replaceEndOfLine(text);
}
const whitespacesOnlyRE = /^[\t\n\f\r ]*$/;
const isWhitespaceOnly = whitespacesOnlyRE.test(text);
const isWhitespaceOnly = htmlWhitespaceUtils.isWhitespaceOnly(text);
const { isFirst, isLast } = path;
if (options.htmlWhitespaceSensitivity !== "ignore") {
// https://infra.spec.whatwg.org/#ascii-whitespace
const leadingWhitespacesRE = /^[\t\n\f\r ]*/;
const trailingWhitespacesRE = /[\t\n\f\r ]*$/;
// let's remove the file's final newline
// https://github.com/ember-cli/ember-new-output/blob/1a04c67ddd02ccb35e0ff41bb5cbce34b31173ef/.editorconfig#L16
const shouldTrimTrailingNewlines =
@@ -234,27 +230,29 @@ function print(path, options, print) {
return breaks;
}
const [lead] = text.match(leadingWhitespacesRE);
const [tail] = text.match(trailingWhitespacesRE);
const leadingWhitespace =
htmlWhitespaceUtils.getLeadingWhitespace(text);
let leadBreaks = [];
if (lead) {
if (leadingWhitespace) {
leadBreaks = [line];
const leadingNewlines = countNewLines(lead);
const leadingNewlines = countNewLines(leadingWhitespace);
if (leadingNewlines) {
leadBreaks = generateHardlines(leadingNewlines);
}
text = text.replace(leadingWhitespacesRE, "");
text = text.slice(leadingWhitespace.length);
}
const tailingWhitespace =
htmlWhitespaceUtils.getTrailingWhitespace(text);
let trailBreaks = [];
if (tail) {
if (tailingWhitespace) {
if (!shouldTrimTrailingNewlines) {
trailBreaks = [line];
const trailingNewlines = countNewLines(tail);
const trailingNewlines = countNewLines(tailingWhitespace);
if (trailingNewlines) {
trailBreaks = generateHardlines(trailingNewlines);
}
@@ -264,7 +262,7 @@ function print(path, options, print) {
}
}
text = text.replace(trailingWhitespacesRE, "");
text = text.slice(0, -tailingWhitespace.length);
}
return [...leadBreaks, fill(getTextValueParts(text)), ...trailBreaks];
@@ -334,9 +332,13 @@ function print(path, options, print) {
trailingSpace = "";
}
text = text
.replaceAll(/^[\t\n\f\r ]+/g, leadingSpace)
.replace(/[\t\n\f\r ]+$/, trailingSpace);
if (htmlWhitespaceUtils.hasLeadingWhitespace(text)) {
text = leadingSpace + htmlWhitespaceUtils.trimStart(text);
}
if (htmlWhitespaceUtils.hasTrailingWhitespace(text)) {
text = htmlWhitespaceUtils.trimEnd(text) + trailingSpace;
}
return [
...generateHardlines(leadingLineBreaksCount),
@@ -643,11 +645,7 @@ function printInverse(path, print, options) {
/* TextNode print helpers */
function getTextValueParts(value) {
return join(line, splitByHtmlWhitespace(value));
}
function splitByHtmlWhitespace(string) {
return string.split(/[\t\n\f\r ]+/);
return join(line, htmlWhitespaceUtils.split(value));
}
function getCurrentAttributeName(path) {
+3 -4
View File
@@ -1,8 +1,7 @@
import { ParseSourceSpan } from "angular-html-parser/lib/compiler/src/parse_util.js";
import htmlWhitespaceUtils from "../utils/html-whitespace-utils.js";
import {
htmlTrim,
getLeadingAndTrailingHtmlWhitespace,
hasHtmlWhitespace,
canHaveInterpolation,
getNodeCssStyleDisplay,
isDanglingSpaceSensitiveNode,
@@ -145,7 +144,7 @@ function mergeSimpleElementIntoText(ast /*, options */) {
node.attrs.length === 0 &&
node.children.length === 1 &&
node.firstChild.type === "text" &&
!hasHtmlWhitespace(node.children[0].value) &&
!htmlWhitespaceUtils.hasWhitespaceCharacter(node.children[0].value) &&
!node.firstChild.hasLeadingSpaces &&
!node.firstChild.hasTrailingSpaces &&
node.isLeadingSpaceSensitive &&
@@ -264,7 +263,7 @@ function extractWhitespaces(ast /*, options*/) {
node.children.length === 0 ||
(node.children.length === 1 &&
node.children[0].type === "text" &&
htmlTrim(node.children[0].value).length === 0)
htmlWhitespaceUtils.trim(node.children[0].value).length === 0)
) {
node.hasDanglingSpaces = node.children.length > 0;
node.children = [];
+16 -18
View File
@@ -12,30 +12,30 @@ import {
CSS_WHITE_SPACE_TAGS,
CSS_WHITE_SPACE_DEFAULT,
} from "../constants.evaluate.js";
import htmlWhitespaceUtils from "../../utils/html-whitespace-utils.js";
import isUnknownNamespace from "./is-unknown-namespace.js";
// https://infra.spec.whatwg.org/#ascii-whitespace
const HTML_WHITESPACE = new Set(["\t", "\n", "\f", "\r", " "]);
const htmlTrimStart = (string) => string.replace(/^[\t\n\f\r ]+/, "");
const htmlTrimEnd = (string) => string.replace(/[\t\n\f\r ]+$/, "");
const htmlTrim = (string) => htmlTrimStart(htmlTrimEnd(string));
const htmlTrimLeadingBlankLines = (string) =>
string.replaceAll(/^[\t\f\r ]*\n/g, "");
const htmlTrimPreserveIndentation = (string) =>
htmlTrimLeadingBlankLines(htmlTrimEnd(string));
const splitByHtmlWhitespace = (string) => string.split(/[\t\n\f\r ]+/);
const getLeadingHtmlWhitespace = (string) => string.match(/^[\t\n\f\r ]*/)[0];
htmlTrimLeadingBlankLines(htmlWhitespaceUtils.trimEnd(string));
const getLeadingAndTrailingHtmlWhitespace = (string) => {
const [, leadingWhitespace, text, trailingWhitespace] = string.match(
/^([\t\n\f\r ]*)(.*?)([\t\n\f\r ]*)$/s
);
let text = string;
const leadingWhitespace = htmlWhitespaceUtils.getLeadingWhitespace(text);
if (leadingWhitespace) {
text = text.slice(leadingWhitespace.length);
}
const trailingWhitespace = htmlWhitespaceUtils.getTrailingWhitespace(text);
if (trailingWhitespace) {
text = text.slice(0, -trailingWhitespace.length);
}
return {
leadingWhitespace,
trailingWhitespace,
text,
};
};
const hasHtmlWhitespace = (string) => /[\t\n\f\r ]/.test(string);
function shouldPreserveContent(node, options) {
// unterminated node in ie conditional comment
@@ -513,12 +513,11 @@ function getMinIndentation(text) {
continue;
}
if (!HTML_WHITESPACE.has(lineText[0])) {
const indentation = htmlWhitespaceUtils.getLeadingWhitespaceCount(lineText);
if (indentation === 0) {
return 0;
}
const indentation = getLeadingHtmlWhitespace(lineText).length;
if (lineText.length === indentation) {
continue;
}
@@ -612,7 +611,7 @@ function getTextValueParts(node, value = node.value) {
dedentString(htmlTrimPreserveIndentation(value)),
hardline
)
: join(line, splitByHtmlWhitespace(value));
: join(line, htmlWhitespaceUtils.split(value));
}
function isVueScriptTag(node, options) {
@@ -620,9 +619,7 @@ function isVueScriptTag(node, options) {
}
export {
htmlTrim,
htmlTrimPreserveIndentation,
hasHtmlWhitespace,
getLeadingAndTrailingHtmlWhitespace,
canHaveInterpolation,
countChars,
@@ -655,4 +652,5 @@ export {
shouldPreserveContent,
unescapeQuoteEntities,
getTextValueParts,
htmlWhitespaceUtils,
};
+18 -24
View File
@@ -17,6 +17,7 @@ import {
import { willBreak, replaceEndOfLine } from "../../document/utils.js";
import UnexpectedNodeError from "../../utils/unexpected-node-error.js";
import getPreferredQuote from "../../utils/get-preferred-quote.js";
import WhitespaceUtils from "../../utils/whitespace-utils.js";
import {
isJsxElement,
rawText,
@@ -32,6 +33,16 @@ import {
import pathNeedsParens from "../needs-parens.js";
import { willPrintOwnComments } from "../comments/printer-methods.js";
/*
Only the following are treated as whitespace inside JSX.
- U+0020 SPACE
- U+000A LF
- U+000D CR
- U+0009 TAB
*/
const jsxWhitespaceUtils = new WhitespaceUtils(" \n\r\t");
const isEmptyStringOrAnyLine = (doc) =>
doc === "" || doc === line || doc === hardline || doc === softline;
@@ -276,7 +287,10 @@ function printJsxChildren(
// Contains a non-whitespace character
if (isMeaningfulJsxText(node)) {
const words = text.split(matchJsxWhitespaceRegex);
const words = jsxWhitespaceUtils.split(
text,
/* captureWhitespace */ true
);
// Starts with whitespace
if (words[0] === "") {
@@ -356,9 +370,8 @@ function printJsxChildren(
const directlyFollowedByMeaningfulText =
next && isMeaningfulJsxText(next);
if (directlyFollowedByMeaningfulText) {
const firstWord = trimJsxWhitespace(rawText(next)).split(
matchJsxWhitespaceRegex
)[0];
const trimmed = jsxWhitespaceUtils.trim(rawText(next));
const [firstWord] = jsxWhitespaceUtils.split(trimmed);
parts.push(
separatorNoWhitespace(isFacebookTranslationTag, firstWord, node, next)
);
@@ -751,25 +764,6 @@ function printJsx(path, options, print) {
}
}
// Only space, newline, carriage return, and tab are treated as whitespace
// inside JSX.
const jsxWhitespaceChars = " \n\r\t";
const matchJsxWhitespaceRegex = new RegExp("([" + jsxWhitespaceChars + "]+)");
const containsNonJsxWhitespaceRegex = new RegExp(
"[^" + jsxWhitespaceChars + "]"
);
const trimJsxWhitespace = (text) =>
text.replace(
new RegExp(
"^" +
matchJsxWhitespaceRegex.source +
"|" +
matchJsxWhitespaceRegex.source +
"$"
),
""
);
/**
* @param {JSXElement} node
* @returns {boolean}
@@ -797,7 +791,7 @@ function isEmptyJsxElement(node) {
function isMeaningfulJsxText(node) {
return (
node.type === "JSXText" &&
(containsNonJsxWhitespaceRegex.test(rawText(node)) ||
(jsxWhitespaceUtils.hasNonWhitespaceCharacter(rawText(node)) ||
!/\n/.test(rawText(node)))
);
}
+7
View File
@@ -0,0 +1,7 @@
import WhitespaceUtils from "./whitespace-utils.js";
// https://infra.spec.whatwg.org/#ascii-whitespace
const HTML_WHITESPACE_CHARACTERS = ["\t", "\n", "\f", "\r", " "];
const htmlWhitespaceUtils = new WhitespaceUtils(HTML_WHITESPACE_CHARACTERS);
export default htmlWhitespaceUtils;
+116
View File
@@ -0,0 +1,116 @@
import escapeStringRegexp from "escape-string-regexp";
class WhitespaceUtils {
#whitespaceCharacters;
constructor(whitespaceCharacters) {
this.#whitespaceCharacters = new Set(whitespaceCharacters);
if (
process.env.NODE_ENV !== "production" &&
(this.#whitespaceCharacters.size === 0 ||
Array.prototype.some.call(
whitespaceCharacters,
(character) => !/^\s$/.test(character)
))
) {
throw new TypeError(
`Invalid characters: ${JSON.stringify(whitespaceCharacters)}`
);
}
}
getLeadingWhitespaceCount(text) {
const whitespaceCharacters = this.#whitespaceCharacters;
let count = 0;
for (
let index = 0;
index < text.length && whitespaceCharacters.has(text.charAt(index));
index++
) {
count++;
}
return count;
}
getTrailingWhitespaceCount(text) {
const whitespaceCharacters = this.#whitespaceCharacters;
let count = 0;
for (
let index = text.length - 1;
index >= 0 && whitespaceCharacters.has(text.charAt(index));
index--
) {
count++;
}
return count;
}
getLeadingWhitespace(text) {
const count = this.getLeadingWhitespaceCount(text);
return text.slice(0, count);
}
getTrailingWhitespace(text) {
const count = this.getTrailingWhitespaceCount(text);
return text.slice(text.length - count);
}
hasLeadingWhitespace(text) {
return this.#whitespaceCharacters.has(text.charAt(0));
}
hasTrailingWhitespace(text) {
return this.#whitespaceCharacters.has(text.at(-1));
}
trimStart(text) {
const count = this.getLeadingWhitespaceCount(text);
return text.slice(count);
}
trimEnd(text) {
const count = this.getTrailingWhitespaceCount(text);
return text.slice(0, text.length - count);
}
trim(text) {
return this.trimEnd(this.trimStart(text));
}
split(text, captureWhitespace = false) {
const pattern = `[${escapeStringRegexp(
[...this.#whitespaceCharacters].join("")
)}]+`;
const regexp = new RegExp(captureWhitespace ? `(${pattern})` : pattern);
return text.split(regexp);
}
hasWhitespaceCharacter(text) {
const whitespaceCharacters = this.#whitespaceCharacters;
return Array.prototype.some.call(text, (character) =>
whitespaceCharacters.has(character)
);
}
hasNonWhitespaceCharacter(text) {
const whitespaceCharacters = this.#whitespaceCharacters;
return Array.prototype.some.call(
text,
(character) => !whitespaceCharacters.has(character)
);
}
isWhitespaceOnly(text) {
const whitespaceCharacters = this.#whitespaceCharacters;
return Array.prototype.every.call(text, (character) =>
whitespaceCharacters.has(character)
);
}
}
export default WhitespaceUtils;
+115
View File
@@ -0,0 +1,115 @@
import WhitespaceUtils from "../../src/utils/whitespace-utils.js";
it("constructor", () => {
expect(() => new WhitespaceUtils()).toThrow(TypeError);
expect(() => new WhitespaceUtils(1)).toThrow(TypeError);
expect(() => new WhitespaceUtils("")).toThrow(TypeError);
expect(() => new WhitespaceUtils([])).toThrow(TypeError);
expect(() => new WhitespaceUtils([""])).toThrow(TypeError);
expect(() => new WhitespaceUtils(["a"])).toThrow(TypeError);
expect(() => new WhitespaceUtils("a")).toThrow(TypeError);
expect(() => new WhitespaceUtils(["\r\n"])).toThrow(TypeError);
expect(new WhitespaceUtils("\r\n")).toBeInstanceOf(WhitespaceUtils);
expect(new WhitespaceUtils(["\r", "\n"])).toBeInstanceOf(WhitespaceUtils);
});
const utils = new WhitespaceUtils(" ");
describe("trimStart", () => {
for (const [string, expected] of [
["", ""],
[" ", ""],
[" ".repeat(3), ""],
["a", "a"],
["a" + " ".repeat(3), "a "],
[" ".repeat(3) + "a", "a"],
[" ".repeat(3) + "a" + " ".repeat(3), "a "],
]) {
it(JSON.stringify(string), () => {
expect(utils.trimStart(string)).toBe(expected);
});
}
});
describe("trimEnd", () => {
for (const [string, expected] of [
["", ""],
[" ", ""],
[" ".repeat(3), ""],
["a", "a"],
["a" + " ".repeat(3), "a"],
[" ".repeat(3) + "a", " a"],
[" ".repeat(3) + "a" + " ".repeat(3), " a"],
]) {
it(JSON.stringify(string), () => {
expect(utils.trimEnd(string)).toBe(expected);
});
}
});
describe("trim", () => {
for (const [string, expected] of [
["", ""],
[" ", ""],
[" ".repeat(3), ""],
["a", "a"],
["a" + " ".repeat(3), "a"],
[" ".repeat(3) + "a", "a"],
[" ".repeat(3) + "a" + " ".repeat(3), "a"],
]) {
it(JSON.stringify(string), () => {
expect(utils.trim(string)).toBe(expected);
});
}
});
describe("getLeadingWhitespaceCount", () => {
for (const [string, expected] of [
["", 0],
[" ", 1],
[" ".repeat(3), 3],
["a", 0],
["a" + " ".repeat(3), 0],
[" ".repeat(3) + "a", 3],
[" ".repeat(3) + "a" + " ".repeat(3), 3],
]) {
it(JSON.stringify(string), () => {
expect(utils.getLeadingWhitespaceCount(string)).toBe(expected);
});
}
});
describe("getTrailingWhitespaceCount", () => {
for (const [string, expected] of [
["", 0],
[" ", 1],
[" ".repeat(3), 3],
["a", 0],
["a" + " ".repeat(3), 3],
[" ".repeat(3) + "a", 0],
[" ".repeat(3) + "a" + " ".repeat(3), 3],
]) {
it(JSON.stringify(string), () => {
expect(utils.getTrailingWhitespaceCount(string)).toBe(expected);
});
}
});
describe("split", () => {
for (const string of [
"",
" ",
" ".repeat(3),
" a",
" ".repeat(3) + "a",
"a ",
"a" + " ".repeat(3),
" a ",
" ".repeat(3) + "a" + " ".repeat(3),
" a a a ",
]) {
it(JSON.stringify(string), () => {
expect(utils.split(string)).toEqual(string.split(/ +/));
});
}
});