prettierx
Version:
prettierX - a less opinionated fork of the Prettier code formatter
731 lines (626 loc) • 18 kB
JavaScript
;
/**
* @typedef {import("../common/ast-path")} AstPath
*/
const htmlTagNames = require("html-tag-names");
const htmlElementAttributes = require("html-element-attributes");
// [prettierx] support --html-void-tags option:
const htmlVoidElements = require("html-void-elements");
const { inferParserByLanguage, isFrontMatterNode } = require("../common/util");
const {
CSS_DISPLAY_TAGS,
CSS_DISPLAY_DEFAULT,
CSS_WHITE_SPACE_TAGS,
CSS_WHITE_SPACE_DEFAULT,
} = require("./constants.evaluate");
const HTML_TAGS = arrayToMap(htmlTagNames);
const HTML_ELEMENT_ATTRIBUTES = mapObject(htmlElementAttributes, arrayToMap);
// [prettierx] support --html-void-tags option:
const HTML_VOID_ELEMENT_SET = new Set(htmlVoidElements);
// 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.replace(/^[\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];
const getLeadingAndTrailingHtmlWhitespace = (string) => {
const [, leadingWhitespace, text, trailingWhitespace] = string.match(
/^([\t\n\f\r ]*)(.*?)([\t\n\f\r ]*)$/s
);
return {
leadingWhitespace,
trailingWhitespace,
text,
};
};
const hasHtmlWhitespace = (string) => /[\t\n\f\r ]/.test(string);
function arrayToMap(array) {
const map = Object.create(null);
for (const value of array) {
map[value] = true;
}
return map;
}
function mapObject(object, fn) {
const newObject = Object.create(null);
for (const [key, value] of Object.entries(object)) {
newObject[key] = fn(value, key);
}
return newObject;
}
function shouldPreserveContent(node, options) {
// unterminated node in ie conditional comment
// e.g. <!--[if lt IE 9]><html><![endif]-->
if (
node.type === "ieConditionalComment" &&
node.lastChild &&
!node.lastChild.isSelfClosing &&
!node.lastChild.endSourceSpan
) {
return true;
}
// incomplete html in ie conditional comment
// e.g. <!--[if lt IE 9]></div><![endif]-->
if (node.type === "ieConditionalComment" && !node.complete) {
return true;
}
// TODO: handle non-text children in <pre>
if (
isPreLikeNode(node) &&
node.children.some(
(child) => child.type !== "text" && child.type !== "interpolation"
)
) {
return true;
}
if (
isVueNonHtmlBlock(node, options) &&
!isScriptLikeTag(node) &&
node.type !== "interpolation"
) {
return true;
}
return false;
}
function hasPrettierIgnore(node) {
/* istanbul ignore next */
if (node.type === "attribute") {
return false;
}
/* istanbul ignore next */
if (!node.parent) {
return false;
}
if (typeof node.index !== "number" || node.index === 0) {
return false;
}
const prevNode = node.parent.children[node.index - 1];
return isPrettierIgnore(prevNode);
}
function isPrettierIgnore(node) {
return node.type === "comment" && node.value.trim() === "prettier-ignore";
}
function getPrettierIgnoreAttributeCommentData(value) {
const match = value.trim().match(/^prettier-ignore-attribute(?:\s+(.+))?$/s);
if (!match) {
return false;
}
if (!match[1]) {
return true;
}
return match[1].split(/\s+/);
}
/** there's no opening/closing tag or it's considered not breakable */
function isTextLikeNode(node) {
return node.type === "text" || node.type === "comment";
}
function isScriptLikeTag(node) {
return (
node.type === "element" &&
(node.fullName === "script" ||
node.fullName === "style" ||
node.fullName === "svg:style" ||
(isUnknownNamespace(node) &&
(node.name === "script" || node.name === "style")))
);
}
function canHaveInterpolation(node) {
return node.children && !isScriptLikeTag(node);
}
function isWhitespaceSensitiveNode(node) {
return (
isScriptLikeTag(node) ||
node.type === "interpolation" ||
isIndentationSensitiveNode(node)
);
}
function isIndentationSensitiveNode(node) {
return getNodeCssStyleWhiteSpace(node).startsWith("pre");
}
function isLeadingSpaceSensitiveNode(node, options) {
const isLeadingSpaceSensitive = _isLeadingSpaceSensitiveNode();
if (
isLeadingSpaceSensitive &&
!node.prev &&
node.parent &&
node.parent.tagDefinition &&
node.parent.tagDefinition.ignoreFirstLf
) {
return node.type === "interpolation";
}
return isLeadingSpaceSensitive;
function _isLeadingSpaceSensitiveNode() {
if (isFrontMatterNode(node)) {
return false;
}
if (
(node.type === "text" || node.type === "interpolation") &&
node.prev &&
(node.prev.type === "text" || node.prev.type === "interpolation")
) {
return true;
}
if (!node.parent || node.parent.cssDisplay === "none") {
return false;
}
if (isPreLikeNode(node.parent)) {
return true;
}
if (
!node.prev &&
(node.parent.type === "root" ||
(isPreLikeNode(node) && node.parent) ||
isScriptLikeTag(node.parent) ||
isVueCustomBlock(node.parent, options) ||
!isFirstChildLeadingSpaceSensitiveCssDisplay(node.parent.cssDisplay))
) {
return false;
}
if (
node.prev &&
!isNextLeadingSpaceSensitiveCssDisplay(node.prev.cssDisplay)
) {
return false;
}
return true;
}
}
function isTrailingSpaceSensitiveNode(node, options) {
if (isFrontMatterNode(node)) {
return false;
}
if (
(node.type === "text" || node.type === "interpolation") &&
node.next &&
(node.next.type === "text" || node.next.type === "interpolation")
) {
return true;
}
if (!node.parent || node.parent.cssDisplay === "none") {
return false;
}
if (isPreLikeNode(node.parent)) {
return true;
}
if (
!node.next &&
(node.parent.type === "root" ||
(isPreLikeNode(node) && node.parent) ||
isScriptLikeTag(node.parent) ||
isVueCustomBlock(node.parent, options) ||
!isLastChildTrailingSpaceSensitiveCssDisplay(node.parent.cssDisplay))
) {
return false;
}
if (
node.next &&
!isPrevTrailingSpaceSensitiveCssDisplay(node.next.cssDisplay)
) {
return false;
}
return true;
}
function isDanglingSpaceSensitiveNode(node) {
return (
isDanglingSpaceSensitiveCssDisplay(node.cssDisplay) &&
!isScriptLikeTag(node)
);
}
function forceNextEmptyLine(node) {
return (
isFrontMatterNode(node) ||
(node.next &&
node.sourceSpan.end &&
node.sourceSpan.end.line + 1 < node.next.sourceSpan.start.line)
);
}
/** firstChild leadingSpaces and lastChild trailingSpaces */
function forceBreakContent(node) {
return (
forceBreakChildren(node) ||
(node.type === "element" &&
node.children.length > 0 &&
(["body", "script", "style"].includes(node.name) ||
node.children.some((child) => hasNonTextChild(child)))) ||
(node.firstChild &&
node.firstChild === node.lastChild &&
node.firstChild.type !== "text" &&
hasLeadingLineBreak(node.firstChild) &&
(!node.lastChild.isTrailingSpaceSensitive ||
hasTrailingLineBreak(node.lastChild)))
);
}
/** spaces between children */
function forceBreakChildren(node) {
return (
node.type === "element" &&
node.children.length > 0 &&
(["html", "head", "ul", "ol", "select"].includes(node.name) ||
(node.cssDisplay.startsWith("table") && node.cssDisplay !== "table-cell"))
);
}
function preferHardlineAsLeadingSpaces(node) {
return (
preferHardlineAsSurroundingSpaces(node) ||
(node.prev && preferHardlineAsTrailingSpaces(node.prev)) ||
hasSurroundingLineBreak(node)
);
}
function preferHardlineAsTrailingSpaces(node) {
return (
preferHardlineAsSurroundingSpaces(node) ||
(node.type === "element" && node.fullName === "br") ||
hasSurroundingLineBreak(node)
);
}
function hasSurroundingLineBreak(node) {
return hasLeadingLineBreak(node) && hasTrailingLineBreak(node);
}
function hasLeadingLineBreak(node) {
return (
node.hasLeadingSpaces &&
(node.prev
? node.prev.sourceSpan.end.line < node.sourceSpan.start.line
: node.parent.type === "root" ||
node.parent.startSourceSpan.end.line < node.sourceSpan.start.line)
);
}
function hasTrailingLineBreak(node) {
return (
node.hasTrailingSpaces &&
(node.next
? node.next.sourceSpan.start.line > node.sourceSpan.end.line
: node.parent.type === "root" ||
(node.parent.endSourceSpan &&
node.parent.endSourceSpan.start.line > node.sourceSpan.end.line))
);
}
function preferHardlineAsSurroundingSpaces(node) {
switch (node.type) {
case "ieConditionalComment":
case "comment":
case "directive":
return true;
case "element":
return ["script", "select"].includes(node.name);
}
return false;
}
function getLastDescendant(node) {
return node.lastChild ? getLastDescendant(node.lastChild) : node;
}
function hasNonTextChild(node) {
return node.children && node.children.some((child) => child.type !== "text");
}
function _inferScriptParser(node) {
const { type, lang } = node.attrMap;
if (
type === "module" ||
type === "text/javascript" ||
type === "text/babel" ||
type === "application/javascript" ||
lang === "jsx"
) {
return "babel";
}
if (type === "application/x-typescript" || lang === "ts" || lang === "tsx") {
return "typescript";
}
if (type === "text/markdown") {
return "markdown";
}
if (type === "text/html") {
return "html";
}
if (type && (type.endsWith("json") || type.endsWith("importmap"))) {
return "json";
}
if (type === "text/x-handlebars-template") {
return "glimmer";
}
}
function inferStyleParser(node) {
const { lang } = node.attrMap;
if (!lang || lang === "postcss" || lang === "css") {
return "css";
}
if (lang === "scss") {
return "scss";
}
if (lang === "less") {
return "less";
}
}
function inferScriptParser(node, options) {
if (node.name === "script" && !node.attrMap.src) {
if (!node.attrMap.lang && !node.attrMap.type) {
return "babel";
}
return _inferScriptParser(node);
}
if (node.name === "style") {
return inferStyleParser(node);
}
if (options && isVueNonHtmlBlock(node, options)) {
return (
_inferScriptParser(node) ||
(!("src" in node.attrMap) &&
inferParserByLanguage(node.attrMap.lang, options))
);
}
}
function isBlockLikeCssDisplay(cssDisplay) {
return (
cssDisplay === "block" ||
cssDisplay === "list-item" ||
cssDisplay.startsWith("table")
);
}
function isFirstChildLeadingSpaceSensitiveCssDisplay(cssDisplay) {
return !isBlockLikeCssDisplay(cssDisplay) && cssDisplay !== "inline-block";
}
function isLastChildTrailingSpaceSensitiveCssDisplay(cssDisplay) {
return !isBlockLikeCssDisplay(cssDisplay) && cssDisplay !== "inline-block";
}
function isPrevTrailingSpaceSensitiveCssDisplay(cssDisplay) {
return !isBlockLikeCssDisplay(cssDisplay);
}
function isNextLeadingSpaceSensitiveCssDisplay(cssDisplay) {
return !isBlockLikeCssDisplay(cssDisplay);
}
function isDanglingSpaceSensitiveCssDisplay(cssDisplay) {
return !isBlockLikeCssDisplay(cssDisplay) && cssDisplay !== "inline-block";
}
function isPreLikeNode(node) {
return getNodeCssStyleWhiteSpace(node).startsWith("pre");
}
/**
* @param {AstPath} path
* @param {(any) => boolean} predicate
*/
function countParents(path, predicate) {
let counter = 0;
for (let i = path.stack.length - 1; i >= 0; i--) {
const value = path.stack[i];
if (
value &&
typeof value === "object" &&
!Array.isArray(value) &&
predicate(value)
) {
counter++;
}
}
return counter;
}
function hasParent(node, fn) {
let current = node;
while (current) {
if (fn(current)) {
return true;
}
current = current.parent;
}
return false;
}
function getNodeCssStyleDisplay(node, options) {
if (node.prev && node.prev.type === "comment") {
// <!-- display: block -->
const match = node.prev.value.match(/^\s*display:\s*([a-z]+)\s*$/);
if (match) {
return match[1];
}
}
let isInSvgForeignObject = false;
if (node.type === "element" && node.namespace === "svg") {
if (hasParent(node, (parent) => parent.fullName === "svg:foreignObject")) {
isInSvgForeignObject = true;
} else {
return node.name === "svg" ? "inline-block" : "block";
}
}
switch (options.htmlWhitespaceSensitivity) {
case "strict":
return "inline";
case "ignore":
return "block";
default: {
// See https://github.com/prettier/prettier/issues/8151
if (
options.parser === "vue" &&
node.parent &&
node.parent.type === "root"
) {
return "block";
}
return (
(node.type === "element" &&
(!node.namespace ||
isInSvgForeignObject ||
isUnknownNamespace(node)) &&
CSS_DISPLAY_TAGS[node.name]) ||
CSS_DISPLAY_DEFAULT
);
}
}
}
function isUnknownNamespace(node) {
return (
node.type === "element" &&
!node.hasExplicitNamespace &&
!["html", "svg"].includes(node.namespace)
);
}
function getNodeCssStyleWhiteSpace(node) {
return (
(node.type === "element" &&
(!node.namespace || isUnknownNamespace(node)) &&
CSS_WHITE_SPACE_TAGS[node.name]) ||
CSS_WHITE_SPACE_DEFAULT
);
}
function getMinIndentation(text) {
let minIndentation = Number.POSITIVE_INFINITY;
for (const lineText of text.split("\n")) {
if (lineText.length === 0) {
continue;
}
if (!HTML_WHITESPACE.has(lineText[0])) {
return 0;
}
const indentation = getLeadingHtmlWhitespace(lineText).length;
if (lineText.length === indentation) {
continue;
}
if (indentation < minIndentation) {
minIndentation = indentation;
}
}
return minIndentation === Number.POSITIVE_INFINITY ? 0 : minIndentation;
}
function dedentString(text, minIndent = getMinIndentation(text)) {
return minIndent === 0
? text
: text
.split("\n")
.map((lineText) => lineText.slice(minIndent))
.join("\n");
}
function shouldNotPrintClosingTag(node, options) {
return (
!node.isSelfClosing &&
!node.endSourceSpan &&
(hasPrettierIgnore(node) || shouldPreserveContent(node.parent, options))
);
}
function countChars(text, char) {
let counter = 0;
for (let i = 0; i < text.length; i++) {
if (text[i] === char) {
counter++;
}
}
return counter;
}
function unescapeQuoteEntities(text) {
return text.replace(/'/g, "'").replace(/"/g, '"');
}
// [prettierx] support --html-void-tags option:
function isHtmlVoidTagNeeded(node, options) {
return (
options.htmlVoidTags &&
options.parser === "html" &&
HTML_VOID_ELEMENT_SET.has(node.fullName)
);
}
// top-level elements (excluding <template>, <style> and <script>) in Vue SFC are considered custom block
// See https://vue-loader.vuejs.org/spec.html for detail
const vueRootElementsSet = new Set(["template", "style", "script"]);
function isVueCustomBlock(node, options) {
return isVueSfcBlock(node, options) && !vueRootElementsSet.has(node.fullName);
}
function isVueSfcBlock(node, options) {
return (
options.parser === "vue" &&
node.type === "element" &&
node.parent.type === "root" &&
node.fullName.toLowerCase() !== "html"
);
}
function isVueNonHtmlBlock(node, options) {
return (
isVueSfcBlock(node, options) &&
(isVueCustomBlock(node, options) ||
(node.attrMap.lang && node.attrMap.lang !== "html"))
);
}
function isVueSlotAttribute(attribute) {
const attributeName = attribute.fullName;
return (
attributeName.charAt(0) === "#" ||
attributeName === "slot-scope" ||
attributeName === "v-slot" ||
attributeName.startsWith("v-slot:")
);
}
function isVueSfcBindingsAttribute(attribute, options) {
const element = attribute.parent;
if (!isVueSfcBlock(element, options)) {
return false;
}
const tagName = element.fullName;
const attributeName = attribute.fullName;
return (
// https://github.com/vuejs/rfcs/blob/sfc-improvements/active-rfcs/0000-sfc-script-setup.md
(tagName === "script" && attributeName === "setup") ||
// https://github.com/vuejs/rfcs/blob/sfc-improvements/active-rfcs/0000-sfc-style-variables.md
(tagName === "style" && attributeName === "vars")
);
}
module.exports = {
HTML_ELEMENT_ATTRIBUTES,
HTML_TAGS,
htmlTrim,
htmlTrimPreserveIndentation,
splitByHtmlWhitespace,
hasHtmlWhitespace,
getLeadingAndTrailingHtmlWhitespace,
canHaveInterpolation,
countChars,
countParents,
dedentString,
forceBreakChildren,
forceBreakContent,
forceNextEmptyLine,
getLastDescendant,
getNodeCssStyleDisplay,
getNodeCssStyleWhiteSpace,
getPrettierIgnoreAttributeCommentData,
hasPrettierIgnore,
inferScriptParser,
isVueCustomBlock,
isVueNonHtmlBlock,
isVueSlotAttribute,
isVueSfcBindingsAttribute,
isDanglingSpaceSensitiveNode,
isIndentationSensitiveNode,
isLeadingSpaceSensitiveNode,
isPreLikeNode,
isScriptLikeTag,
isTextLikeNode,
isTrailingSpaceSensitiveNode,
isWhitespaceSensitiveNode,
isUnknownNamespace,
preferHardlineAsLeadingSpaces,
preferHardlineAsTrailingSpaces,
shouldNotPrintClosingTag,
shouldPreserveContent,
unescapeQuoteEntities,
// [prettierx] support --html-void-tags option:
isHtmlVoidTagNeeded,
};