UNPKG

prettierx

Version:

prettierX - a less opinionated fork of the Prettier code formatter

731 lines (626 loc) 18 kB
"use strict"; /** * @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(/&apos;/g, "'").replace(/&quot;/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, };