diff --git a/src/common/util.js b/src/common/util.js deleted file mode 100644 index 2a0c0e3a7..000000000 --- a/src/common/util.js +++ /dev/null @@ -1,389 +0,0 @@ -import escapeStringRegexp from "escape-string-regexp"; -import { - skip, - skipWhitespace, - skipSpaces, - skipToLineEnd, - skipEverythingButNewLine, -} from "../utils/text/skip.js"; -import skipInlineComment from "../utils/text/skip-inline-comment.js"; -import skipTrailingComment from "../utils/text/skip-trailing-comment.js"; -import skipNewline from "../utils/text/skip-newline.js"; -import getNextNonSpaceNonCommentCharacterIndex from "../utils/text/get-next-non-space-non-comment-character-index.js"; - -/** - * @typedef {{backwards?: boolean}} SkipOptions - */ - -/** - * @param {string} text - * @param {number} startIndex - * @param {SkipOptions=} options - * @returns {boolean} - */ -function hasNewline(text, startIndex, options = {}) { - const idx = skipSpaces( - text, - options.backwards ? startIndex - 1 : startIndex, - options - ); - const idx2 = skipNewline(text, idx, options); - return idx !== idx2; -} - -/** - * @param {string} text - * @param {number} startIndex - * @param {number} endIndex - * @returns {boolean} - */ -function hasNewlineInRange(text, startIndex, endIndex) { - for (let i = startIndex; i < endIndex; ++i) { - if (text.charAt(i) === "\n") { - return true; - } - } - return false; -} - -// Note: this function doesn't ignore leading comments unlike isNextLineEmpty -/** - * @param {string} text - * @param {number} startIndex - * @returns {boolean} - */ -function isPreviousLineEmpty(text, startIndex) { - /** @type {number | false} */ - let idx = startIndex - 1; - idx = skipSpaces(text, idx, { backwards: true }); - idx = skipNewline(text, idx, { backwards: true }); - idx = skipSpaces(text, idx, { backwards: true }); - const idx2 = skipNewline(text, idx, { backwards: true }); - return idx !== idx2; -} - -/** - * @param {string} text - * @param {number} startIndex - * @returns {boolean} - */ -function isNextLineEmpty(text, startIndex) { - /** @type {number | false} */ - let oldIdx = null; - /** @type {number | false} */ - let idx = startIndex; - while (idx !== oldIdx) { - // We need to skip all the potential trailing inline comments - oldIdx = idx; - idx = skipToLineEnd(text, idx); - idx = skipInlineComment(text, idx); - idx = skipSpaces(text, idx); - } - idx = skipTrailingComment(text, idx); - idx = skipNewline(text, idx); - return idx !== false && hasNewline(text, idx); -} - -/** - * @param {string} text - * @param {number} startIndex - * @returns {string} - */ -function getNextNonSpaceNonCommentCharacter(text, startIndex) { - const index = getNextNonSpaceNonCommentCharacterIndex(text, startIndex); - return index === false ? "" : text.charAt(index); -} - -// Not using, but it's public utils -/* c8 ignore start */ -/** - * @param {string} text - * @param {number} startIndex - * @param {SkipOptions=} options - * @returns {boolean} - */ -function hasSpaces(text, startIndex, options = {}) { - const idx = skipSpaces( - text, - options.backwards ? startIndex - 1 : startIndex, - options - ); - return idx !== startIndex; -} -/* c8 ignore stop */ - -/** - * @param {string} text - * @param {number} tabWidth - * @param {number=} startIndex - * @returns {number} - */ -function getAlignmentSize(text, tabWidth, startIndex = 0) { - let size = 0; - for (let i = startIndex; i < text.length; ++i) { - if (text[i] === "\t") { - // Tabs behave in a way that they are aligned to the nearest - // multiple of tabWidth: - // 0 -> 4, 1 -> 4, 2 -> 4, 3 -> 4 - // 4 -> 8, 5 -> 8, 6 -> 8, 7 -> 8 ... - size = size + tabWidth - (size % tabWidth); - } else { - size++; - } - } - - return size; -} - -/** - * @param {string} value - * @param {number} tabWidth - * @returns {number} - */ -function getIndentSize(value, tabWidth) { - const lastNewlineIndex = value.lastIndexOf("\n"); - /* c8 ignore next 3 */ - if (lastNewlineIndex === -1) { - return 0; - } - - return getAlignmentSize( - // All the leading whitespaces - value.slice(lastNewlineIndex + 1).match(/^[\t ]*/)[0], - tabWidth - ); -} - -/** - * @typedef {'"' | "'"} Quote - */ - -/** - * - * @param {string} rawContent - * @param {Quote} preferredQuote - * @returns {{ quote: Quote, regex: RegExp, escaped: string }} - */ - -function getPreferredQuote(rawContent, preferredQuote) { - /** @type {{ quote: '"', regex: RegExp, escaped: """ }} */ - const double = { quote: '"', regex: /"/g, escaped: """ }; - /** @type {{ quote: "'", regex: RegExp, escaped: "'" }} */ - const single = { quote: "'", regex: /'/g, escaped: "'" }; - - const preferred = preferredQuote === "'" ? single : double; - const alternate = preferred === single ? double : single; - - let result = preferred; - - // If `rawContent` contains at least one of the quote preferred for enclosing - // the string, we might want to enclose with the alternate quote instead, to - // minimize the number of escaped quotes. - if ( - rawContent.includes(preferred.quote) || - rawContent.includes(alternate.quote) - ) { - const numPreferredQuotes = (rawContent.match(preferred.regex) || []).length; - const numAlternateQuotes = (rawContent.match(alternate.regex) || []).length; - - result = numPreferredQuotes > numAlternateQuotes ? alternate : preferred; - } - - return result; -} - -function printString(raw, options) { - // `rawContent` is the string exactly like it appeared in the input source - // code, without its enclosing quotes. - const rawContent = raw.slice(1, -1); - - /** @type {Quote} */ - const enclosingQuote = - options.parser === "json" || - (options.parser === "json5" && - options.quoteProps === "preserve" && - !options.singleQuote) - ? '"' - : options.__isInHtmlAttribute - ? "'" - : getPreferredQuote(rawContent, options.singleQuote ? "'" : '"').quote; - - // It might sound unnecessary to use `makeString` even if the string already - // is enclosed with `enclosingQuote`, but it isn't. The string could contain - // unnecessary escapes (such as in `"\'"`). Always using `makeString` makes - // sure that we consistently output the minimum amount of escaped quotes. - return makeString( - rawContent, - enclosingQuote, - !( - options.parser === "css" || - options.parser === "less" || - options.parser === "scss" || - options.__embeddedInHtml - ) - ); -} - -/** - * @param {string} rawText - * @param {Quote} enclosingQuote - * @param {boolean=} unescapeUnnecessaryEscapes - * @returns {string} - */ -function makeString(rawText, enclosingQuote, unescapeUnnecessaryEscapes) { - const otherQuote = enclosingQuote === '"' ? "'" : '"'; - - // Matches _any_ escape and unescaped quotes (both single and double). - const regex = /\\(.)|(["'])/gs; - - // Escape and unescape single and double quotes as needed to be able to - // enclose `rawText` with `enclosingQuote`. - const raw = rawText.replaceAll(regex, (match, escaped, quote) => { - // If we matched an escape, and the escaped character is a quote of the - // other type than we intend to enclose the string with, there's no need for - // it to be escaped, so return it _without_ the backslash. - if (escaped === otherQuote) { - return escaped; - } - - // If we matched an unescaped quote and it is of the _same_ type as we - // intend to enclose the string with, it must be escaped, so return it with - // a backslash. - if (quote === enclosingQuote) { - return "\\" + quote; - } - - if (quote) { - return quote; - } - - // Unescape any unnecessarily escaped character. - // Adapted from https://github.com/eslint/eslint/blob/de0b4ad7bd820ade41b1f606008bea68683dc11a/lib/rules/no-useless-escape.js#L27 - return unescapeUnnecessaryEscapes && - /^[^\n\r"'0-7\\bfnrt-vx\u2028\u2029]$/.test(escaped) - ? escaped - : "\\" + escaped; - }); - - return enclosingQuote + raw + enclosingQuote; -} - -function printNumber(rawNumber) { - return ( - rawNumber - .toLowerCase() - // Remove unnecessary plus and zeroes from scientific notation. - .replace(/^([+-]?[\d.]+e)(?:\+|(-))?0*(?=\d)/, "$1$2") - // Remove unnecessary scientific notation (1e0). - .replace(/^([+-]?[\d.]+)e[+-]?0+$/, "$1") - // Make sure numbers always start with a digit. - .replace(/^([+-])?\./, "$10.") - // Remove extraneous trailing decimal zeroes. - .replace(/(\.\d+?)0+(?=e|$)/, "$1") - // Remove trailing dot. - .replace(/\.(?=e|$)/, "") - ); -} - -/** - * @param {string} text - * @param {string} searchString - * @returns {number} - */ -function getMaxContinuousCount(text, searchString) { - const results = text.match( - new RegExp(`(${escapeStringRegexp(searchString)})+`, "g") - ); - - if (results === null) { - return 0; - } - - return results.reduce( - (maxCount, result) => - Math.max(maxCount, result.length / searchString.length), - 0 - ); -} - -/** - * @param {string} text - * @param {string} searchString - * @returns {number} - */ -function getMinNotPresentContinuousCount(text, searchString) { - const matches = text.match( - new RegExp(`(${escapeStringRegexp(searchString)})+`, "g") - ); - - if (matches === null) { - return 0; - } - - const countPresent = new Map(); - let max = 0; - - for (const match of matches) { - const count = match.length / searchString.length; - countPresent.set(count, true); - if (count > max) { - max = count; - } - } - - for (let i = 1; i < max; i++) { - if (!countPresent.get(i)) { - return i; - } - } - - return max + 1; -} - -function isFrontMatterNode(node) { - return node?.type === "front-matter"; -} - -/** - * @param {string} description - * @returns {(node: any) => symbol} - */ -function createGroupIdMapper(description) { - const groupIds = new WeakMap(); - return function (node) { - if (!groupIds.has(node)) { - groupIds.set(node, Symbol(description)); - } - return groupIds.get(node); - }; -} - -export { - getMaxContinuousCount, - getMinNotPresentContinuousCount, - getNextNonSpaceNonCommentCharacterIndex, - getNextNonSpaceNonCommentCharacter, - skip, - skipWhitespace, - skipSpaces, - skipToLineEnd, - skipEverythingButNewLine, - skipInlineComment, - skipTrailingComment, - skipNewline, - isNextLineEmpty, - isPreviousLineEmpty, - hasNewline, - hasNewlineInRange, - hasSpaces, - getAlignmentSize, - getIndentSize, - getPreferredQuote, - printString, - printNumber, - makeString, - isFrontMatterNode, - createGroupIdMapper, -}; -export { default as isNonEmptyArray } from "../utils/is-non-empty-array.js"; -export { default as getStringWidth } from "../utils/get-string-width.js"; diff --git a/src/index.cjs b/src/index.cjs index 1f1659a40..818a95393 100644 --- a/src/index.cjs +++ b/src/index.cjs @@ -39,7 +39,7 @@ for (const name of debugApiFunctionNames) { prettier.__debug = debugApis; if (process.env.NODE_ENV === "production") { - prettier.util = require("./common/util-shared.js"); + prettier.util = require("./utils/public.js"); prettier.doc = require("./document/index.js"); } else { Object.defineProperties(prettier, { diff --git a/src/index.js b/src/index.js index 0f444fc83..970dba735 100644 --- a/src/index.js +++ b/src/index.js @@ -20,7 +20,7 @@ import { hiddenDefaults as optionsHiddenDefaults } from "./main/options.js"; import normalizeOptions from "./main/options-normalizer.js"; import arrayify from "./utils/arrayify.js"; import partition from "./utils/partition.js"; -import { isNonEmptyArray } from "./common/util.js"; +import isNonEmptyArray from "./utils/is-non-empty-array.js"; const builtinPlugins = Object.values(languages); @@ -110,6 +110,6 @@ export { sharedWithCli as __internal, debugApis as __debug, }; -export * as util from "./common/util-shared.js"; +export * as util from "./utils/public.js"; export * as doc from "./document/index.js"; export { default as version } from "./main/version.evaluate.cjs"; diff --git a/src/language-css/clean.js b/src/language-css/clean.js index 4b5658197..314c76766 100644 --- a/src/language-css/clean.js +++ b/src/language-css/clean.js @@ -1,4 +1,4 @@ -import { isFrontMatterNode } from "../common/util.js"; +import isFrontMatter from "../utils/front-matter/is-front-matter.js"; const ignoredProperties = new Set([ "raw", // front-matter @@ -12,7 +12,7 @@ const ignoredProperties = new Set([ ]); function clean(ast, newObj, parent) { - if (isFrontMatterNode(ast) && ast.lang === "yaml") { + if (isFrontMatter(ast) && ast.lang === "yaml") { delete newObj.value; } @@ -25,7 +25,7 @@ function clean(ast, newObj, parent) { // first non-front-matter comment if ( parent.nodes[0] === ast || - (isFrontMatterNode(parent.nodes[0]) && parent.nodes[1] === ast) + (isFrontMatter(parent.nodes[0]) && parent.nodes[1] === ast) ) { /** * something diff --git a/src/language-css/loc.js b/src/language-css/loc.js index 2cf488c35..77c2f3408 100644 --- a/src/language-css/loc.js +++ b/src/language-css/loc.js @@ -1,4 +1,4 @@ -import { skipEverythingButNewLine } from "../utils/text/skip.js"; +import { skipEverythingButNewLine } from "../utils/skip.js"; import isNonEmptyArray from "../utils/is-non-empty-array.js"; import lineColumnToIndex from "../utils/line-column-to-index.js"; diff --git a/src/language-css/printer-postcss.js b/src/language-css/printer-postcss.js index b57e4d1ed..2db2bc126 100644 --- a/src/language-css/printer-postcss.js +++ b/src/language-css/printer-postcss.js @@ -1,11 +1,9 @@ -import { - printNumber, - printString, - hasNewline, - isFrontMatterNode, - isNextLineEmpty, - isNonEmptyArray, -} from "../common/util.js"; +import printNumber from "../utils/print-number.js"; +import printString from "../utils/print-string.js"; +import hasNewline from "../utils/has-newline.js"; +import isFrontMatter from "../utils/front-matter/is-front-matter.js"; +import isNextLineEmpty from "../utils/is-next-line-empty.js"; +import isNonEmptyArray from "../utils/is-non-empty-array.js"; import { join, line, @@ -1100,7 +1098,7 @@ function printNodeSequence(path, options, print) { !hasNewline(options.originalText, locStart(next), { backwards: true, }) && - !isFrontMatterNode(node)) || + !isFrontMatter(node)) || (next.type === "css-atrule" && next.name === "else" && node.type !== "css-comment") @@ -1110,7 +1108,7 @@ function printNodeSequence(path, options, print) { parts.push(options.__isHTMLStyleAttribute ? line : hardline); if ( isNextLineEmpty(options.originalText, locEnd(node)) && - !isFrontMatterNode(node) + !isFrontMatter(node) ) { parts.push(hardline); } diff --git a/src/language-graphql/printer-graphql.js b/src/language-graphql/printer-graphql.js index 32d638127..a92ecb73d 100644 --- a/src/language-graphql/printer-graphql.js +++ b/src/language-graphql/printer-graphql.js @@ -7,7 +7,8 @@ import { indent, ifBreak, } from "../document/builders.js"; -import { isNextLineEmpty, isNonEmptyArray } from "../common/util.js"; +import isNextLineEmpty from "../utils/is-next-line-empty.js"; +import isNonEmptyArray from "../utils/is-non-empty-array.js"; import UnexpectedNodeError from "../utils/unexpected-node-error.js"; import { insertPragma } from "./pragma.js"; import { locStart, locEnd } from "./loc.js"; diff --git a/src/language-handlebars/printer-glimmer.js b/src/language-handlebars/printer-glimmer.js index 5169dd221..41c7d0ad6 100644 --- a/src/language-handlebars/printer-glimmer.js +++ b/src/language-handlebars/printer-glimmer.js @@ -10,7 +10,8 @@ import { softline, } from "../document/builders.js"; import { replaceEndOfLine } from "../document/utils.js"; -import { getPreferredQuote, isNonEmptyArray } from "../common/util.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 { locStart, locEnd } from "./loc.js"; import clean from "./clean.js"; @@ -686,7 +687,7 @@ function generateHardlines(number = 0) { /* StringLiteral print helpers */ -/** @typedef {import("../common/util.js").Quote} Quote */ +/** @typedef {import("../utils/get-preferred-quote.js").Quote} Quote */ /** * Prints a string literal with the correct surrounding quotes based on diff --git a/src/language-html/clean.js b/src/language-html/clean.js index 1a3ab5afe..7734924e4 100644 --- a/src/language-html/clean.js +++ b/src/language-html/clean.js @@ -1,4 +1,4 @@ -import { isFrontMatterNode } from "../common/util.js"; +import isFrontMatter from "../utils/front-matter/is-front-matter.js"; const ignoredProperties = new Set([ "sourceSpan", @@ -18,7 +18,7 @@ function clean(ast, newNode) { } // may be formatted by multiparser - if (isFrontMatterNode(ast) || ast.type === "yaml" || ast.type === "toml") { + if (isFrontMatter(ast) || ast.type === "yaml" || ast.type === "toml") { return null; } diff --git a/src/language-html/print/tag.js b/src/language-html/print/tag.js index ff600be76..0a51407d6 100644 --- a/src/language-html/print/tag.js +++ b/src/language-html/print/tag.js @@ -3,7 +3,7 @@ */ import assert from "node:assert"; -import { isNonEmptyArray } from "../../common/util.js"; +import isNonEmptyArray from "../../utils/is-non-empty-array.js"; import { indent, join, diff --git a/src/language-html/utils/index.js b/src/language-html/utils/index.js index de6f4b73f..3d57ceb8a 100644 --- a/src/language-html/utils/index.js +++ b/src/language-html/utils/index.js @@ -2,7 +2,7 @@ * @typedef {import("../../common/ast-path.js").default} AstPath */ -import { isFrontMatterNode } from "../../common/util.js"; +import isFrontMatter from "../../utils/front-matter/is-front-matter.js"; import inferParserByLanguage from "../../utils/infer-parser-by-language.js"; import { line, hardline, join } from "../../document/builders.js"; import { replaceEndOfLine } from "../../document/utils.js"; @@ -144,7 +144,7 @@ function isLeadingSpaceSensitiveNode(node, options) { return isLeadingSpaceSensitive; function _isLeadingSpaceSensitiveNode() { - if (isFrontMatterNode(node)) { + if (isFrontMatter(node)) { return false; } @@ -187,7 +187,7 @@ function isLeadingSpaceSensitiveNode(node, options) { } function isTrailingSpaceSensitiveNode(node, options) { - if (isFrontMatterNode(node)) { + if (isFrontMatter(node)) { return false; } @@ -237,7 +237,7 @@ function isDanglingSpaceSensitiveNode(node) { function forceNextEmptyLine(node) { return ( - isFrontMatterNode(node) || + isFrontMatter(node) || (node.next && node.sourceSpan.end && node.sourceSpan.end.line + 1 < node.next.sourceSpan.start.line) diff --git a/src/language-js/comments/handle-comments.js b/src/language-js/comments/handle-comments.js index bd8b7c2cb..f2c717301 100644 --- a/src/language-js/comments/handle-comments.js +++ b/src/language-js/comments/handle-comments.js @@ -1,10 +1,8 @@ -import { - hasNewline, - getNextNonSpaceNonCommentCharacterIndex, - getNextNonSpaceNonCommentCharacter, - hasNewlineInRange, - isNonEmptyArray, -} from "../../common/util.js"; +import hasNewline from "../../utils/has-newline.js"; +import getNextNonSpaceNonCommentCharacter from "../../utils/get-next-non-space-non-comment-character.js"; +import getNextNonSpaceNonCommentCharacterIndex from "../../utils/get-next-non-space-non-comment-character-index.js"; +import hasNewlineInRange from "../../utils/has-newline-in-range.js"; +import isNonEmptyArray from "../../utils/is-non-empty-array.js"; import { addLeadingComment, addTrailingComment, diff --git a/src/language-js/embed/css.js b/src/language-js/embed/css.js index 254cffee1..1838986ab 100644 --- a/src/language-js/embed/css.js +++ b/src/language-js/embed/css.js @@ -1,4 +1,4 @@ -import { isNonEmptyArray } from "../../common/util.js"; +import isNonEmptyArray from "../../utils/is-non-empty-array.js"; import { indent, hardline, softline } from "../../document/builders.js"; import { mapDoc, replaceEndOfLine, cleanDoc } from "../../document/utils.js"; import { printTemplateExpressions } from "../print/template-literal.js"; diff --git a/src/language-js/parse/babel.js b/src/language-js/parse/babel.js index 95dc65492..ce71094a2 100644 --- a/src/language-js/parse/babel.js +++ b/src/language-js/parse/babel.js @@ -1,6 +1,6 @@ import tryCombinations from "../../utils/try-combinations.js"; import getShebang from "../utils/get-shebang.js"; -import getNextNonSpaceNonCommentCharacterIndex from "../../utils/text/get-next-non-space-non-comment-character-index.js"; +import getNextNonSpaceNonCommentCharacterIndex from "../../utils/get-next-non-space-non-comment-character-index.js"; // JSON parsers are bundled here so we can reduce package size import jsonParsers from "../../language-json/parser-json.js"; import createParser from "./utils/create-parser.js"; diff --git a/src/language-js/print/array.js b/src/language-js/print/array.js index e0a5d5be2..36ab8d56e 100644 --- a/src/language-js/print/array.js +++ b/src/language-js/print/array.js @@ -8,7 +8,7 @@ import { ifBreak, fill, } from "../../document/builders.js"; -import { hasNewline } from "../../common/util.js"; +import hasNewline from "../../utils/has-newline.js"; import { shouldPrintComma, hasComment, diff --git a/src/language-js/print/assignment.js b/src/language-js/print/assignment.js index 91e4c0414..7907c6763 100644 --- a/src/language-js/print/assignment.js +++ b/src/language-js/print/assignment.js @@ -1,4 +1,5 @@ -import { isNonEmptyArray, getStringWidth } from "../../common/util.js"; +import isNonEmptyArray from "../../utils/is-non-empty-array.js"; +import getStringWidth from "../../utils/get-string-width.js"; import { line, group, diff --git a/src/language-js/print/block.js b/src/language-js/print/block.js index 996701ffc..5fec3b4ab 100644 --- a/src/language-js/print/block.js +++ b/src/language-js/print/block.js @@ -1,5 +1,5 @@ import { printDanglingComments } from "../../main/comments/print.js"; -import { isNonEmptyArray } from "../../common/util.js"; +import isNonEmptyArray from "../../utils/is-non-empty-array.js"; import { hardline, indent } from "../../document/builders.js"; import { hasComment, diff --git a/src/language-js/print/class.js b/src/language-js/print/class.js index 28b5c7836..1bf386656 100644 --- a/src/language-js/print/class.js +++ b/src/language-js/print/class.js @@ -1,4 +1,5 @@ -import { isNonEmptyArray, createGroupIdMapper } from "../../common/util.js"; +import isNonEmptyArray from "../../utils/is-non-empty-array.js"; +import createGroupIdMapper from "../../utils/create-group-id-mapper.js"; import { printComments, printDanglingComments, diff --git a/src/language-js/print/decorators.js b/src/language-js/print/decorators.js index 8d97ed937..1c4f5602e 100644 --- a/src/language-js/print/decorators.js +++ b/src/language-js/print/decorators.js @@ -1,4 +1,5 @@ -import { isNonEmptyArray, hasNewline } from "../../common/util.js"; +import isNonEmptyArray from "../../utils/is-non-empty-array.js"; +import hasNewline from "../../utils/has-newline.js"; import { line, hardline, diff --git a/src/language-js/print/flow.js b/src/language-js/print/flow.js index d715e4136..7adc8a876 100644 --- a/src/language-js/print/flow.js +++ b/src/language-js/print/flow.js @@ -1,7 +1,8 @@ /** @typedef {import("../../document/builders.js").Doc} Doc */ import assert from "node:assert"; -import { printString, printNumber } from "../../common/util.js"; +import printString from "../../utils/print-string.js"; +import printNumber from "../../utils/print-number.js"; import { replaceEndOfLine } from "../../document/utils.js"; import UnexpectedNodeError from "../../utils/unexpected-node-error.js"; import { diff --git a/src/language-js/print/function-parameters.js b/src/language-js/print/function-parameters.js index 90786560f..b449f25fa 100644 --- a/src/language-js/print/function-parameters.js +++ b/src/language-js/print/function-parameters.js @@ -1,4 +1,4 @@ -import { getNextNonSpaceNonCommentCharacter } from "../../common/util.js"; +import getNextNonSpaceNonCommentCharacter from "../../utils/get-next-non-space-non-comment-character.js"; import { printDanglingComments } from "../../main/comments/print.js"; import { line, diff --git a/src/language-js/print/function.js b/src/language-js/print/function.js index ec2f96412..3a40deac3 100644 --- a/src/language-js/print/function.js +++ b/src/language-js/print/function.js @@ -5,7 +5,7 @@ import { printDanglingComments, printCommentsSeparately, } from "../../main/comments/print.js"; -import { getNextNonSpaceNonCommentCharacterIndex } from "../../common/util.js"; +import getNextNonSpaceNonCommentCharacterIndex from "../../utils/get-next-non-space-non-comment-character-index.js"; import { line, softline, diff --git a/src/language-js/print/interface.js b/src/language-js/print/interface.js index accf78316..25e9334cc 100644 --- a/src/language-js/print/interface.js +++ b/src/language-js/print/interface.js @@ -1,4 +1,4 @@ -import { isNonEmptyArray } from "../../common/util.js"; +import isNonEmptyArray from "../../utils/is-non-empty-array.js"; import { join, line, group, indent, ifBreak } from "../../document/builders.js"; import { hasComment, identity, CommentCheckFlags } from "../utils/index.js"; import { getTypeParametersGroupId } from "./type-parameters.js"; diff --git a/src/language-js/print/jsx.js b/src/language-js/print/jsx.js index 5d5ceef6b..9a696dfa8 100644 --- a/src/language-js/print/jsx.js +++ b/src/language-js/print/jsx.js @@ -16,8 +16,7 @@ import { } from "../../document/builders.js"; import { willBreak, replaceEndOfLine } from "../../document/utils.js"; import UnexpectedNodeError from "../../utils/unexpected-node-error.js"; - -import { getPreferredQuote } from "../../common/util.js"; +import getPreferredQuote from "../../utils/get-preferred-quote.js"; import { isJsxElement, rawText, diff --git a/src/language-js/print/literal.js b/src/language-js/print/literal.js index ce088555d..2bc3be42e 100644 --- a/src/language-js/print/literal.js +++ b/src/language-js/print/literal.js @@ -1,4 +1,5 @@ -import { printString, printNumber } from "../../common/util.js"; +import printString from "../../utils/print-string.js"; +import printNumber from "../../utils/print-number.js"; import { replaceEndOfLine } from "../../document/utils.js"; import { printDirective } from "./misc.js"; diff --git a/src/language-js/print/member-chain.js b/src/language-js/print/member-chain.js index 139d6d970..f5cece850 100644 --- a/src/language-js/print/member-chain.js +++ b/src/language-js/print/member-chain.js @@ -1,8 +1,6 @@ import { printComments } from "../../main/comments/print.js"; -import { - isNextLineEmpty as isNextLineEmptyAfterIndex, - getNextNonSpaceNonCommentCharacterIndex, -} from "../../common/util.js"; +import isNextLineEmptyAfterIndex from "../../utils/is-next-line-empty.js"; +import getNextNonSpaceNonCommentCharacterIndex from "../../utils/get-next-non-space-non-comment-character-index.js"; import pathNeedsParens from "../needs-parens.js"; import { isCallExpression, diff --git a/src/language-js/print/module.js b/src/language-js/print/module.js index 7b3e81fa9..dba85ec4f 100644 --- a/src/language-js/print/module.js +++ b/src/language-js/print/module.js @@ -1,4 +1,4 @@ -import { isNonEmptyArray } from "../../common/util.js"; +import isNonEmptyArray from "../../utils/is-non-empty-array.js"; import UnexpectedNodeError from "../../utils/unexpected-node-error.js"; import { softline, diff --git a/src/language-js/print/object.js b/src/language-js/print/object.js index 437c664aa..4122a8d4a 100644 --- a/src/language-js/print/object.js +++ b/src/language-js/print/object.js @@ -7,11 +7,9 @@ import { ifBreak, hardline, } from "../../document/builders.js"; -import { - hasNewlineInRange, - hasNewline, - isNonEmptyArray, -} from "../../common/util.js"; +import hasNewlineInRange from "../../utils/has-newline-in-range.js"; +import hasNewline from "../../utils/has-newline.js"; +import isNonEmptyArray from "../../utils/is-non-empty-array.js"; import { shouldPrintComma, hasComment, diff --git a/src/language-js/print/property.js b/src/language-js/print/property.js index eface35b7..36f19e55d 100644 --- a/src/language-js/print/property.js +++ b/src/language-js/print/property.js @@ -1,5 +1,6 @@ import { printComments } from "../../main/comments/print.js"; -import { printString, printNumber } from "../../common/util.js"; +import printString from "../../utils/print-string.js"; +import printNumber from "../../utils/print-number.js"; import { isNumericLiteral, isSimpleNumber, diff --git a/src/language-js/print/template-literal.js b/src/language-js/print/template-literal.js index c99ddc3c6..c4c868b17 100644 --- a/src/language-js/print/template-literal.js +++ b/src/language-js/print/template-literal.js @@ -1,4 +1,5 @@ -import { getStringWidth, getIndentSize } from "../../common/util.js"; +import getStringWidth from "../../utils/get-string-width.js"; +import getIndentSize from "../../utils/get-indent-size.js"; import { join, hardline, diff --git a/src/language-js/print/ternary.js b/src/language-js/print/ternary.js index 4de1be574..5d842de2f 100644 --- a/src/language-js/print/ternary.js +++ b/src/language-js/print/ternary.js @@ -1,4 +1,4 @@ -import { hasNewlineInRange } from "../../common/util.js"; +import hasNewlineInRange from "../../utils/has-newline-in-range.js"; import { isJsxElement, isCallExpression, diff --git a/src/language-js/print/type-parameters.js b/src/language-js/print/type-parameters.js index c135d892a..4e24ee86b 100644 --- a/src/language-js/print/type-parameters.js +++ b/src/language-js/print/type-parameters.js @@ -18,7 +18,7 @@ import { isObjectType, getTypeScriptMappedTypeModifier, } from "../utils/index.js"; -import { createGroupIdMapper } from "../../common/util.js"; +import createGroupIdMapper from "../../utils/create-group-id-mapper.js"; import { printTypeAnnotationProperty, shouldHugType, diff --git a/src/language-js/print/typescript.js b/src/language-js/print/typescript.js index 0824029e3..30b185b3f 100644 --- a/src/language-js/print/typescript.js +++ b/src/language-js/print/typescript.js @@ -1,5 +1,5 @@ import { printDanglingComments } from "../../main/comments/print.js"; -import { hasNewlineInRange } from "../../common/util.js"; +import hasNewlineInRange from "../../utils/has-newline-in-range.js"; import { join, line, diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js index da6b1c44f..dbe0269fe 100644 --- a/src/language-js/printer-estree.js +++ b/src/language-js/printer-estree.js @@ -1,6 +1,6 @@ // TODO(azz): anything that imports from main shouldn't be in a `language-*` dir. import { printDanglingComments } from "../main/comments/print.js"; -import { hasNewline } from "../common/util.js"; +import hasNewline from "../utils/has-newline.js"; import { join, line, diff --git a/src/language-js/utils/index.js b/src/language-js/utils/index.js index 2b021cbb4..dfee8e933 100644 --- a/src/language-js/utils/index.js +++ b/src/language-js/utils/index.js @@ -1,10 +1,8 @@ import isEs5IdentifierName from "@prettier/is-es5-identifier-name"; -import { - hasNewline, - isNonEmptyArray, - isNextLineEmpty as isNextLineEmptyAfterIndex, - getStringWidth, -} from "../../common/util.js"; +import hasNewline from "../../utils/has-newline.js"; +import isNonEmptyArray from "../../utils/is-non-empty-array.js"; +import isNextLineEmptyAfterIndex from "../../utils/is-next-line-empty.js"; +import getStringWidth from "../../utils/get-string-width.js"; import { locStart, locEnd, hasSameLocStart } from "../loc.js"; import getVisitorKeys from "../traverse/get-visitor-keys.js"; import isBlockComment from "./is-block-comment.js"; diff --git a/src/language-markdown/clean.js b/src/language-markdown/clean.js index f92e31210..c9e663b1c 100644 --- a/src/language-markdown/clean.js +++ b/src/language-markdown/clean.js @@ -1,5 +1,5 @@ import collapseWhiteSpace from "collapse-white-space"; -import { isFrontMatterNode } from "../common/util.js"; +import isFrontMatter from "../utils/front-matter/is-front-matter.js"; import { startWithPragma } from "./pragma.js"; const ignoredProperties = new Set([ @@ -63,7 +63,7 @@ function clean(ast, newObj, parent) { parent?.type === "root" && parent.children.length > 0 && (parent.children[0] === ast || - (isFrontMatterNode(parent.children[0]) && parent.children[1] === ast)) && + (isFrontMatter(parent.children[0]) && parent.children[1] === ast)) && ast.type === "html" && startWithPragma(ast.value) ) { diff --git a/src/language-markdown/embed.js b/src/language-markdown/embed.js index 547e90091..1e4c509ba 100644 --- a/src/language-markdown/embed.js +++ b/src/language-markdown/embed.js @@ -1,4 +1,4 @@ -import { getMaxContinuousCount } from "../common/util.js"; +import getMaxContinuousCount from "../utils/get-max-continuous-count.js"; import { hardline, markAsRoot } from "../document/builders.js"; import { replaceEndOfLine } from "../document/utils.js"; import printFrontMatter from "../utils/front-matter/print.js"; diff --git a/src/language-markdown/printer-markdown.js b/src/language-markdown/printer-markdown.js index 09130ab1c..68b71ff61 100644 --- a/src/language-markdown/printer-markdown.js +++ b/src/language-markdown/printer-markdown.js @@ -1,9 +1,7 @@ import collapseWhiteSpace from "collapse-white-space"; -import { - getMinNotPresentContinuousCount, - getMaxContinuousCount, - getStringWidth, -} from "../common/util.js"; +import getMinNotPresentContinuousCount from "../utils/get-min-not-present-continuous-count.js"; +import getMaxContinuousCount from "../utils/get-max-continuous-count.js"; +import getStringWidth from "../utils/get-string-width.js"; import { breakParent, join, diff --git a/src/language-yaml/printer-yaml.js b/src/language-yaml/printer-yaml.js index a721d11f5..0168c26ac 100644 --- a/src/language-yaml/printer-yaml.js +++ b/src/language-yaml/printer-yaml.js @@ -10,7 +10,7 @@ import { lineSuffix, } from "../document/builders.js"; import { replaceEndOfLine } from "../document/utils.js"; -import { isPreviousLineEmpty } from "../common/util.js"; +import isPreviousLineEmpty from "../utils/is-previous-line-empty.js"; import UnexpectedNodeError from "../utils/unexpected-node-error.js"; import { insertPragma, isPragma } from "./pragma.js"; import { locStart } from "./loc.js"; diff --git a/src/language-yaml/utils.js b/src/language-yaml/utils.js index 8981844ab..7706b5657 100644 --- a/src/language-yaml/utils.js +++ b/src/language-yaml/utils.js @@ -1,4 +1,4 @@ -import { isNonEmptyArray } from "../common/util.js"; +import isNonEmptyArray from "../utils/is-non-empty-array.js"; /** * @param {any} value diff --git a/src/main/comments/attach.js b/src/main/comments/attach.js index 19c82f45a..444dc97c3 100644 --- a/src/main/comments/attach.js +++ b/src/main/comments/attach.js @@ -1,5 +1,6 @@ import assert from "node:assert"; -import { hasNewline, isNonEmptyArray } from "../../common/util.js"; +import hasNewline from "../../utils/has-newline.js"; +import isNonEmptyArray from "../../utils/is-non-empty-array.js"; import createGetVisitorKeysFunction from "../create-get-visitor-keys-function.js"; import { addLeadingComment, diff --git a/src/main/comments/print.js b/src/main/comments/print.js index f5314a177..4c2a25f54 100644 --- a/src/main/comments/print.js +++ b/src/main/comments/print.js @@ -7,13 +7,11 @@ import { join, label, } from "../../document/builders.js"; -import { - hasNewline, - skipNewline, - skipSpaces, - isPreviousLineEmpty, - isNonEmptyArray, -} from "../../common/util.js"; +import hasNewline from "../../utils/has-newline.js"; +import skipNewline from "../../utils/skip-newline.js"; +import { skipSpaces } from "../../utils/skip.js"; +import isPreviousLineEmpty from "../../utils/is-previous-line-empty.js"; +import isNonEmptyArray from "../../utils/is-non-empty-array.js"; /** * @typedef {import("../../common/ast-path.js").default} AstPath diff --git a/src/main/core.js b/src/main/core.js index 346f671c6..fa3929158 100644 --- a/src/main/core.js +++ b/src/main/core.js @@ -2,7 +2,7 @@ import { diffArrays } from "diff"; import { printDocToString as printDocToStringWithoutNormalizeOptions } from "../document/printer.js"; import { printDocToDebug } from "../document/debug.js"; -import { getAlignmentSize } from "../common/util.js"; +import getAlignmentSize from "../utils/get-alignment-size.js"; import { guessEndOfLine, convertEndOfLineToChars, diff --git a/src/standalone.js b/src/standalone.js index b955e52d3..e00a2e747 100644 --- a/src/standalone.js +++ b/src/standalone.js @@ -57,6 +57,6 @@ export { getSupportInfo, debugApis as __debug, }; -export * as util from "./common/util-shared.js"; +export * as util from "./utils/public.js"; export * as doc from "./document/index.js"; export { default as version } from "./main/version.evaluate.cjs"; diff --git a/src/utils/create-group-id-mapper.js b/src/utils/create-group-id-mapper.js new file mode 100644 index 000000000..25fa15b2c --- /dev/null +++ b/src/utils/create-group-id-mapper.js @@ -0,0 +1,15 @@ +/** + * @param {string} description + * @returns {(node: any) => symbol} + */ +function createGroupIdMapper(description) { + const groupIds = new WeakMap(); + return function (node) { + if (!groupIds.has(node)) { + groupIds.set(node, Symbol(description)); + } + return groupIds.get(node); + }; +} + +export default createGroupIdMapper; diff --git a/src/utils/front-matter/is-front-matter.js b/src/utils/front-matter/is-front-matter.js new file mode 100644 index 000000000..4ef2b5b32 --- /dev/null +++ b/src/utils/front-matter/is-front-matter.js @@ -0,0 +1,5 @@ +function isFrontMatter(node) { + return node?.type === "front-matter"; +} + +export default isFrontMatter; diff --git a/src/utils/get-alignment-size.js b/src/utils/get-alignment-size.js new file mode 100644 index 000000000..526821ee4 --- /dev/null +++ b/src/utils/get-alignment-size.js @@ -0,0 +1,24 @@ +/** + * @param {string} text + * @param {number} tabWidth + * @param {number=} startIndex + * @returns {number} + */ +function getAlignmentSize(text, tabWidth, startIndex = 0) { + let size = 0; + for (let i = startIndex; i < text.length; ++i) { + if (text[i] === "\t") { + // Tabs behave in a way that they are aligned to the nearest + // multiple of tabWidth: + // 0 -> 4, 1 -> 4, 2 -> 4, 3 -> 4 + // 4 -> 8, 5 -> 8, 6 -> 8, 7 -> 8 ... + size = size + tabWidth - (size % tabWidth); + } else { + size++; + } + } + + return size; +} + +export default getAlignmentSize; diff --git a/src/utils/get-indent-size.js b/src/utils/get-indent-size.js new file mode 100644 index 000000000..6d3067f13 --- /dev/null +++ b/src/utils/get-indent-size.js @@ -0,0 +1,22 @@ +import getAlignmentSize from "./get-alignment-size.js"; + +/** + * @param {string} value + * @param {number} tabWidth + * @returns {number} + */ +function getIndentSize(value, tabWidth) { + const lastNewlineIndex = value.lastIndexOf("\n"); + /* c8 ignore next 3 */ + if (lastNewlineIndex === -1) { + return 0; + } + + return getAlignmentSize( + // All the leading whitespaces + value.slice(lastNewlineIndex + 1).match(/^[\t ]*/)[0], + tabWidth + ); +} + +export default getIndentSize; diff --git a/src/utils/get-max-continuous-count.js b/src/utils/get-max-continuous-count.js new file mode 100644 index 000000000..efe27a4f8 --- /dev/null +++ b/src/utils/get-max-continuous-count.js @@ -0,0 +1,24 @@ +import escapeStringRegexp from "escape-string-regexp"; + +/** + * @param {string} text + * @param {string} searchString + * @returns {number} + */ +function getMaxContinuousCount(text, searchString) { + const results = text.match( + new RegExp(`(${escapeStringRegexp(searchString)})+`, "g") + ); + + if (results === null) { + return 0; + } + + return results.reduce( + (maxCount, result) => + Math.max(maxCount, result.length / searchString.length), + 0 + ); +} + +export default getMaxContinuousCount; diff --git a/src/utils/get-min-not-present-continuous-count.js b/src/utils/get-min-not-present-continuous-count.js new file mode 100644 index 000000000..c0d278201 --- /dev/null +++ b/src/utils/get-min-not-present-continuous-count.js @@ -0,0 +1,37 @@ +import escapeStringRegexp from "escape-string-regexp"; + +/** + * @param {string} text + * @param {string} searchString + * @returns {number} + */ +function getMinNotPresentContinuousCount(text, searchString) { + const matches = text.match( + new RegExp(`(${escapeStringRegexp(searchString)})+`, "g") + ); + + if (matches === null) { + return 0; + } + + const countPresent = new Map(); + let max = 0; + + for (const match of matches) { + const count = match.length / searchString.length; + countPresent.set(count, true); + if (count > max) { + max = count; + } + } + + for (let i = 1; i < max; i++) { + if (!countPresent.get(i)) { + return i; + } + } + + return max + 1; +} + +export default getMinNotPresentContinuousCount; diff --git a/src/utils/text/get-next-non-space-non-comment-character-index.js b/src/utils/get-next-non-space-non-comment-character-index.js similarity index 100% rename from src/utils/text/get-next-non-space-non-comment-character-index.js rename to src/utils/get-next-non-space-non-comment-character-index.js diff --git a/src/utils/get-next-non-space-non-comment-character.js b/src/utils/get-next-non-space-non-comment-character.js new file mode 100644 index 000000000..b303050c0 --- /dev/null +++ b/src/utils/get-next-non-space-non-comment-character.js @@ -0,0 +1,13 @@ +import getNextNonSpaceNonCommentCharacterIndex from "./get-next-non-space-non-comment-character-index.js"; + +/** + * @param {string} text + * @param {number} startIndex + * @returns {string} + */ +function getNextNonSpaceNonCommentCharacter(text, startIndex) { + const index = getNextNonSpaceNonCommentCharacterIndex(text, startIndex); + return index === false ? "" : text.charAt(index); +} + +export default getNextNonSpaceNonCommentCharacter; diff --git a/src/utils/get-preferred-quote.js b/src/utils/get-preferred-quote.js new file mode 100644 index 000000000..700b5e81d --- /dev/null +++ b/src/utils/get-preferred-quote.js @@ -0,0 +1,39 @@ +/** + * @typedef {'"' | "'"} Quote + */ + +/** + * + * @param {string} rawContent + * @param {Quote} preferredQuote + * @returns {{ quote: Quote, regex: RegExp, escaped: string }} + */ + +function getPreferredQuote(rawContent, preferredQuote) { + /** @type {{ quote: '"', regex: RegExp, escaped: """ }} */ + const double = { quote: '"', regex: /"/g, escaped: """ }; + /** @type {{ quote: "'", regex: RegExp, escaped: "'" }} */ + const single = { quote: "'", regex: /'/g, escaped: "'" }; + + const preferred = preferredQuote === "'" ? single : double; + const alternate = preferred === single ? double : single; + + let result = preferred; + + // If `rawContent` contains at least one of the quote preferred for enclosing + // the string, we might want to enclose with the alternate quote instead, to + // minimize the number of escaped quotes. + if ( + rawContent.includes(preferred.quote) || + rawContent.includes(alternate.quote) + ) { + const numPreferredQuotes = (rawContent.match(preferred.regex) || []).length; + const numAlternateQuotes = (rawContent.match(alternate.regex) || []).length; + + result = numPreferredQuotes > numAlternateQuotes ? alternate : preferred; + } + + return result; +} + +export default getPreferredQuote; diff --git a/src/utils/has-newline-in-range.js b/src/utils/has-newline-in-range.js new file mode 100644 index 000000000..a32b99534 --- /dev/null +++ b/src/utils/has-newline-in-range.js @@ -0,0 +1,16 @@ +/** + * @param {string} text + * @param {number} startIndex + * @param {number} endIndex + * @returns {boolean} + */ +function hasNewlineInRange(text, startIndex, endIndex) { + for (let i = startIndex; i < endIndex; ++i) { + if (text.charAt(i) === "\n") { + return true; + } + } + return false; +} + +export default hasNewlineInRange; diff --git a/src/utils/has-newline.js b/src/utils/has-newline.js new file mode 100644 index 000000000..2340fb444 --- /dev/null +++ b/src/utils/has-newline.js @@ -0,0 +1,22 @@ +import { skipSpaces } from "./skip.js"; +import skipNewline from "./skip-newline.js"; + +/** @typedef {import("./skip.js").SkipOptions} SkipOptions */ + +/** + * @param {string} text + * @param {number} startIndex + * @param {SkipOptions=} options + * @returns {boolean} + */ +function hasNewline(text, startIndex, options = {}) { + const idx = skipSpaces( + text, + options.backwards ? startIndex - 1 : startIndex, + options + ); + const idx2 = skipNewline(text, idx, options); + return idx !== idx2; +} + +export default hasNewline; diff --git a/src/utils/has-spaces.js b/src/utils/has-spaces.js new file mode 100644 index 000000000..6eef55abc --- /dev/null +++ b/src/utils/has-spaces.js @@ -0,0 +1,23 @@ +import { skipSpaces } from "./skip.js"; + +/** @typedef {import("./skip.js").SkipOptions} SkipOptions */ + +// Not using, but it's public utils +/* c8 ignore start */ +/** + * @param {string} text + * @param {number} startIndex + * @param {SkipOptions=} options + * @returns {boolean} + */ +function hasSpaces(text, startIndex, options = {}) { + const idx = skipSpaces( + text, + options.backwards ? startIndex - 1 : startIndex, + options + ); + return idx !== startIndex; +} +/* c8 ignore stop */ + +export default hasSpaces; diff --git a/src/utils/is-next-line-empty.js b/src/utils/is-next-line-empty.js new file mode 100644 index 000000000..bef5c0423 --- /dev/null +++ b/src/utils/is-next-line-empty.js @@ -0,0 +1,29 @@ +import skipNewline from "./skip-newline.js"; +import { skipSpaces, skipToLineEnd } from "./skip.js"; +import skipInlineComment from "./skip-inline-comment.js"; +import skipTrailingComment from "./skip-trailing-comment.js"; +import hasNewline from "./has-newline.js"; + +/** + * @param {string} text + * @param {number} startIndex + * @returns {boolean} + */ +function isNextLineEmpty(text, startIndex) { + /** @type {number | false} */ + let oldIdx = null; + /** @type {number | false} */ + let idx = startIndex; + while (idx !== oldIdx) { + // We need to skip all the potential trailing inline comments + oldIdx = idx; + idx = skipToLineEnd(text, idx); + idx = skipInlineComment(text, idx); + idx = skipSpaces(text, idx); + } + idx = skipTrailingComment(text, idx); + idx = skipNewline(text, idx); + return idx !== false && hasNewline(text, idx); +} + +export default isNextLineEmpty; diff --git a/src/utils/is-previous-line-empty.js b/src/utils/is-previous-line-empty.js new file mode 100644 index 000000000..2820787a1 --- /dev/null +++ b/src/utils/is-previous-line-empty.js @@ -0,0 +1,20 @@ +import skipNewline from "./skip-newline.js"; +import { skipSpaces } from "./skip.js"; + +// Note: this function doesn't ignore leading comments unlike isNextLineEmpty +/** + * @param {string} text + * @param {number} startIndex + * @returns {boolean} + */ +function isPreviousLineEmpty(text, startIndex) { + /** @type {number | false} */ + let idx = startIndex - 1; + idx = skipSpaces(text, idx, { backwards: true }); + idx = skipNewline(text, idx, { backwards: true }); + idx = skipSpaces(text, idx, { backwards: true }); + const idx2 = skipNewline(text, idx, { backwards: true }); + return idx !== idx2; +} + +export default isPreviousLineEmpty; diff --git a/src/utils/make-string.js b/src/utils/make-string.js new file mode 100644 index 000000000..37eab2690 --- /dev/null +++ b/src/utils/make-string.js @@ -0,0 +1,47 @@ +/** @typedef {import("./get-preferred-quote.js").Quote} Quote */ + +/** + * @param {string} rawText + * @param {Quote} enclosingQuote + * @param {boolean=} unescapeUnnecessaryEscapes + * @returns {string} + */ +function makeString(rawText, enclosingQuote, unescapeUnnecessaryEscapes) { + const otherQuote = enclosingQuote === '"' ? "'" : '"'; + + // Matches _any_ escape and unescaped quotes (both single and double). + const regex = /\\(.)|(["'])/gs; + + // Escape and unescape single and double quotes as needed to be able to + // enclose `rawText` with `enclosingQuote`. + const raw = rawText.replaceAll(regex, (match, escaped, quote) => { + // If we matched an escape, and the escaped character is a quote of the + // other type than we intend to enclose the string with, there's no need for + // it to be escaped, so return it _without_ the backslash. + if (escaped === otherQuote) { + return escaped; + } + + // If we matched an unescaped quote and it is of the _same_ type as we + // intend to enclose the string with, it must be escaped, so return it with + // a backslash. + if (quote === enclosingQuote) { + return "\\" + quote; + } + + if (quote) { + return quote; + } + + // Unescape any unnecessarily escaped character. + // Adapted from https://github.com/eslint/eslint/blob/de0b4ad7bd820ade41b1f606008bea68683dc11a/lib/rules/no-useless-escape.js#L27 + return unescapeUnnecessaryEscapes && + /^[^\n\r"'0-7\\bfnrt-vx\u2028\u2029]$/.test(escaped) + ? escaped + : "\\" + escaped; + }); + + return enclosingQuote + raw + enclosingQuote; +} + +export default makeString; diff --git a/src/utils/print-number.js b/src/utils/print-number.js new file mode 100644 index 000000000..489c30ef3 --- /dev/null +++ b/src/utils/print-number.js @@ -0,0 +1,18 @@ +function printNumber(rawNumber) { + return ( + rawNumber + .toLowerCase() + // Remove unnecessary plus and zeroes from scientific notation. + .replace(/^([+-]?[\d.]+e)(?:\+|(-))?0*(?=\d)/, "$1$2") + // Remove unnecessary scientific notation (1e0). + .replace(/^([+-]?[\d.]+)e[+-]?0+$/, "$1") + // Make sure numbers always start with a digit. + .replace(/^([+-])?\./, "$10.") + // Remove extraneous trailing decimal zeroes. + .replace(/(\.\d+?)0+(?=e|$)/, "$1") + // Remove trailing dot. + .replace(/\.(?=e|$)/, "") + ); +} + +export default printNumber; diff --git a/src/utils/print-string.js b/src/utils/print-string.js new file mode 100644 index 000000000..21b86308b --- /dev/null +++ b/src/utils/print-string.js @@ -0,0 +1,38 @@ +import getPreferredQuote from "./get-preferred-quote.js"; +import makeString from "./make-string.js"; + +/** @typedef {import("./get-preferred-quote.js").Quote} Quote */ + +function printString(raw, options) { + // `rawContent` is the string exactly like it appeared in the input source + // code, without its enclosing quotes. + const rawContent = raw.slice(1, -1); + + /** @type {Quote} */ + const enclosingQuote = + options.parser === "json" || + (options.parser === "json5" && + options.quoteProps === "preserve" && + !options.singleQuote) + ? '"' + : options.__isInHtmlAttribute + ? "'" + : getPreferredQuote(rawContent, options.singleQuote ? "'" : '"').quote; + + // It might sound unnecessary to use `makeString` even if the string already + // is enclosed with `enclosingQuote`, but it isn't. The string could contain + // unnecessary escapes (such as in `"\'"`). Always using `makeString` makes + // sure that we consistently output the minimum amount of escaped quotes. + return makeString( + rawContent, + enclosingQuote, + !( + options.parser === "css" || + options.parser === "less" || + options.parser === "scss" || + options.__embeddedInHtml + ) + ); +} + +export default printString; diff --git a/src/common/util-shared.js b/src/utils/public.js similarity index 68% rename from src/common/util-shared.js rename to src/utils/public.js index 58b288561..01558ce09 100644 --- a/src/common/util-shared.js +++ b/src/utils/public.js @@ -1,8 +1,6 @@ -import { - getNextNonSpaceNonCommentCharacterIndex as getNextNonSpaceNonCommentCharacterIndexWithStartIndex, - isPreviousLineEmpty as isPreviousLineEmptyWithStartIndex, - isNextLineEmpty as isNextLineEmptyAfterIndex, -} from "./util.js"; +import getNextNonSpaceNonCommentCharacterIndexWithStartIndex from "./get-next-non-space-non-comment-character-index.js"; +import isPreviousLineEmptyWithStartIndex from "./is-previous-line-empty.js"; +import isNextLineEmptyAfterIndex from "./is-next-line-empty.js"; // Legacy version of `getNextNonSpaceNonCommentCharacterIndex` /** @@ -84,29 +82,30 @@ export function isNextLineEmpty(text, startIndex) { legacyIsNextLineEmpty(...arguments); } +export { default as getMaxContinuousCount } from "./get-max-continuous-count.js"; +export { default as getStringWidth } from "./get-string-width.js"; +export { default as getAlignmentSize } from "./get-alignment-size.js"; +export { default as getIndentSize } from "./get-indent-size.js"; +export { default as skipNewline } from "./skip-newline.js"; +export { default as skipInlineComment } from "./skip-inline-comment.js"; +export { default as skipTrailingComment } from "./skip-trailing-comment.js"; +export { default as hasNewline } from "./has-newline.js"; +export { default as hasNewlineInRange } from "./has-newline-in-range.js"; +export { default as hasSpaces } from "./has-spaces.js"; +export { default as getNextNonSpaceNonCommentCharacter } from "./get-next-non-space-non-comment-character.js"; +export { default as makeString } from "./make-string.js"; export { - getMaxContinuousCount, - getStringWidth, - getAlignmentSize, - getIndentSize, skip, skipWhitespace, skipSpaces, - skipNewline, skipToLineEnd, skipEverythingButNewLine, - skipInlineComment, - skipTrailingComment, - hasNewline, - hasNewlineInRange, - hasSpaces, - getNextNonSpaceNonCommentCharacter, - makeString, - // Remove this in v4 - isNextLineEmpty as isNextLineEmptyAfterIndex, -} from "./util.js"; +} from "./skip.js"; export { addLeadingComment, addDanglingComment, addTrailingComment, } from "../main/comments/utils.js"; + +// TODO: Remove this in v4 +export { isNextLineEmptyAfterIndex }; diff --git a/src/utils/text/skip-inline-comment.js b/src/utils/skip-inline-comment.js similarity index 100% rename from src/utils/text/skip-inline-comment.js rename to src/utils/skip-inline-comment.js diff --git a/src/utils/text/skip-newline.js b/src/utils/skip-newline.js similarity index 100% rename from src/utils/text/skip-newline.js rename to src/utils/skip-newline.js diff --git a/src/utils/text/skip-trailing-comment.js b/src/utils/skip-trailing-comment.js similarity index 100% rename from src/utils/text/skip-trailing-comment.js rename to src/utils/skip-trailing-comment.js diff --git a/src/utils/text/skip.js b/src/utils/skip.js similarity index 100% rename from src/utils/text/skip.js rename to src/utils/skip.js