"use strict"; const clean = require("./clean"); const { builders, utils: { stripTrailingHardline, mapDoc } } = require("../doc"); const { breakParent, dedentToRoot, fill, group, hardline, ifBreak, indent, join, line, literalline, markAsRoot, softline } = builders; const { countChars, countParents, dedentString, forceBreakChildren, forceBreakContent, forceNextEmptyLine, getLastDescendant, getPrettierIgnoreAttributeCommentData, hasPrettierIgnore, inferScriptParser, isScriptLikeTag, isTextLikeNode, normalizeParts, preferHardlineAsLeadingSpaces, shouldNotPrintClosingTag, shouldPreserveContent, unescapeQuoteEntities } = require("./utils"); const { replaceEndOfLineWith } = require("../common/util"); const preprocess = require("./preprocess"); const assert = require("assert"); const { insertPragma } = require("./pragma"); const { printVueFor, printVueSlotScope, isVueEventBindingExpression } = require("./syntax-vue"); const { printImgSrcset } = require("./syntax-attribute"); function concat(parts) { const newParts = normalizeParts(parts); return newParts.length === 0 ? "" : newParts.length === 1 ? newParts[0] : builders.concat(newParts); } function embed(path, print, textToDoc, options) { const node = path.getValue(); switch (node.type) { case "text": { if (isScriptLikeTag(node.parent)) { const parser = inferScriptParser(node.parent); if (parser) { const value = parser === "markdown" ? dedentString(node.value.replace(/^[^\S\n]*?\n/, "")) : node.value; return builders.concat([ concat([ breakParent, printOpeningTagPrefix(node, options), stripTrailingHardline(textToDoc(value, { parser })), printClosingTagSuffix(node, options) ]) ]); } } else if (node.parent.type === "interpolation") { return concat([ indent( concat([ line, textToDoc( node.value, options.parser === "angular" ? { parser: "__ng_interpolation", trailingComma: "none" } : options.parser === "vue" ? { parser: "__vue_expression" } : { parser: "__js_expression" } ) ]) ), node.parent.next && needsToBorrowPrevClosingTagEndMarker(node.parent.next) ? " " : line ]); } break; } case "attribute": { if (!node.value) { break; } // lit-html: html`` if ( /^PRETTIER_HTML_PLACEHOLDER_\d+_IN_JS$/.test( options.originalText.slice( node.valueSpan.start.offset, node.valueSpan.end.offset ) ) ) { return concat([node.rawName, "=", node.value]); } const embeddedAttributeValueDoc = printEmbeddedAttributeValue( node, (code, opts) => // strictly prefer single quote to avoid unnecessary html entity escape textToDoc(code, Object.assign({ __isInHtmlAttribute: true }, opts)), options ); if (embeddedAttributeValueDoc) { return concat([ node.rawName, '="', group( mapDoc(embeddedAttributeValueDoc, doc => typeof doc === "string" ? doc.replace(/"/g, """) : doc ) ), '"' ]); } break; } case "yaml": return markAsRoot( concat([ "---", hardline, node.value.trim().length === 0 ? "" : textToDoc(node.value, { parser: "yaml" }), "---" ]) ); } } function genericPrint(path, options, print) { const node = path.getValue(); switch (node.type) { case "root": // use original concat to not break stripTrailingHardline return builders.concat([ group(printChildren(path, options, print)), hardline ]); case "element": case "ieConditionalComment": { /** * do not break: * *
{{ * ~ * interpolation * }}
* ~ * * exception: break if the opening tag breaks * *
{{ * interpolation * }}
*/ const shouldHugContent = node.children.length === 1 && node.firstChild.type === "interpolation" && (node.firstChild.isLeadingSpaceSensitive && !node.firstChild.hasLeadingSpaces) && (node.lastChild.isTrailingSpaceSensitive && !node.lastChild.hasTrailingSpaces); const attrGroupId = Symbol("element-attr-group-id"); return concat([ group( concat([ group(printOpeningTag(path, options, print), { id: attrGroupId }), node.children.length === 0 ? node.hasDanglingSpaces && node.isDanglingSpaceSensitive ? line : "" : concat([ forceBreakContent(node) ? breakParent : "", (childrenDoc => shouldHugContent ? ifBreak(indent(childrenDoc), childrenDoc, { groupId: attrGroupId }) : isScriptLikeTag(node) && node.parent.type === "root" && options.parser === "vue" ? childrenDoc : indent(childrenDoc))( concat([ shouldHugContent ? ifBreak(softline, "", { groupId: attrGroupId }) : node.firstChild.hasLeadingSpaces && node.firstChild.isLeadingSpaceSensitive ? line : node.firstChild.type === "text" && node.isWhitespaceSensitive && node.isIndentationSensitive ? dedentToRoot(softline) : softline, printChildren(path, options, print) ]) ), (node.next ? needsToBorrowPrevClosingTagEndMarker(node.next) : needsToBorrowLastChildClosingTagEndMarker(node.parent)) ? node.lastChild.hasTrailingSpaces && node.lastChild.isTrailingSpaceSensitive ? " " : "" : shouldHugContent ? ifBreak(softline, "", { groupId: attrGroupId }) : node.lastChild.hasTrailingSpaces && node.lastChild.isTrailingSpaceSensitive ? line : (node.lastChild.type === "comment" || (node.lastChild.type === "text" && node.isWhitespaceSensitive && node.isIndentationSensitive)) && new RegExp( `\\n\\s{${options.tabWidth * countParents( path, n => n.parent && n.parent.type !== "root" )}}$` ).test(node.lastChild.value) ? /** *
*
                       *         something
                       *       
* ~ *
*/ "" : softline ]) ]) ), printClosingTag(node, options) ]); } case "ieConditionalStartComment": case "ieConditionalEndComment": return concat([printOpeningTagStart(node), printClosingTagEnd(node)]); case "interpolation": return concat([ printOpeningTagStart(node, options), concat(path.map(print, "children")), printClosingTagEnd(node, options) ]); case "text": { if (node.parent.type === "interpolation") { // replace the trailing literalline with hardline for better readability const trailingNewlineRegex = /\n[^\S\n]*?$/; const hasTrailingNewline = trailingNewlineRegex.test(node.value); const value = hasTrailingNewline ? node.value.replace(trailingNewlineRegex, "") : node.value; return concat([ concat(replaceEndOfLineWith(value, literalline)), hasTrailingNewline ? hardline : "" ]); } return fill( normalizeParts( [].concat( printOpeningTagPrefix(node, options), getTextValueParts(node), printClosingTagSuffix(node, options) ) ) ); } case "docType": return concat([ group( concat([ printOpeningTagStart(node, options), " ", node.value.replace(/^html\b/i, "html").replace(/\s+/g, " ") ]) ), printClosingTagEnd(node, options) ]); case "comment": { return concat([ printOpeningTagPrefix(node, options), concat( replaceEndOfLineWith( options.originalText.slice( options.locStart(node), options.locEnd(node) ), literalline ) ), printClosingTagSuffix(node, options) ]); } case "attribute": { if (node.value === null) { return node.rawName; } const value = unescapeQuoteEntities(node.value); const singleQuoteCount = countChars(value, "'"); const doubleQuoteCount = countChars(value, '"'); const quote = singleQuoteCount < doubleQuoteCount ? "'" : '"'; return concat([ node.rawName, concat([ "=", quote, concat( replaceEndOfLineWith( quote === '"' ? value.replace(/"/g, """) : value.replace(/'/g, "'"), literalline ) ), quote ]) ]); } case "yaml": case "toml": return concat(replaceEndOfLineWith(node.raw, literalline)); default: throw new Error(`Unexpected node type ${node.type}`); } } function printChildren(path, options, print) { const node = path.getValue(); if (forceBreakChildren(node)) { return concat([ breakParent, concat( path.map(childPath => { const childNode = childPath.getValue(); const prevBetweenLine = !childNode.prev ? "" : printBetweenLine(childNode.prev, childNode); return concat([ !prevBetweenLine ? "" : concat([ prevBetweenLine, forceNextEmptyLine(childNode.prev) ? hardline : "" ]), printChild(childPath) ]); }, "children") ) ]); } const groupIds = node.children.map(() => Symbol("")); return concat( path.map((childPath, childIndex) => { const childNode = childPath.getValue(); if (isTextLikeNode(childNode)) { if (childNode.prev && isTextLikeNode(childNode.prev)) { const prevBetweenLine = printBetweenLine(childNode.prev, childNode); if (prevBetweenLine) { if (forceNextEmptyLine(childNode.prev)) { return concat([hardline, hardline, printChild(childPath)]); } return concat([prevBetweenLine, printChild(childPath)]); } } return printChild(childPath); } const prevParts = []; const leadingParts = []; const trailingParts = []; const nextParts = []; const prevBetweenLine = childNode.prev ? printBetweenLine(childNode.prev, childNode) : ""; const nextBetweenLine = childNode.next ? printBetweenLine(childNode, childNode.next) : ""; if (prevBetweenLine) { if (forceNextEmptyLine(childNode.prev)) { prevParts.push(hardline, hardline); } else if (prevBetweenLine === hardline) { prevParts.push(hardline); } else { if (isTextLikeNode(childNode.prev)) { leadingParts.push(prevBetweenLine); } else { leadingParts.push( ifBreak("", softline, { groupId: groupIds[childIndex - 1] }) ); } } } if (nextBetweenLine) { if (forceNextEmptyLine(childNode)) { if (isTextLikeNode(childNode.next)) { nextParts.push(hardline, hardline); } } else if (nextBetweenLine === hardline) { if (isTextLikeNode(childNode.next)) { nextParts.push(hardline); } } else { trailingParts.push(nextBetweenLine); } } return concat( [].concat( prevParts, group( concat([ concat(leadingParts), group(concat([printChild(childPath), concat(trailingParts)]), { id: groupIds[childIndex] }) ]) ), nextParts ) ); }, "children") ); function printChild(childPath) { const child = childPath.getValue(); if (hasPrettierIgnore(child)) { return concat( [].concat( printOpeningTagPrefix(child, options), replaceEndOfLineWith( options.originalText.slice( options.locStart(child) + (child.prev && needsToBorrowNextOpeningTagStartMarker(child.prev) ? printOpeningTagStartMarker(child).length : 0), options.locEnd(child) - (child.next && needsToBorrowPrevClosingTagEndMarker(child.next) ? printClosingTagEndMarker(child, options).length : 0) ), literalline ), printClosingTagSuffix(child, options) ) ); } if (shouldPreserveContent(child, options)) { return concat( [].concat( printOpeningTagPrefix(child, options), group(printOpeningTag(childPath, options, print)), replaceEndOfLineWith( options.originalText.slice( child.startSourceSpan.end.offset + (child.firstChild && needsToBorrowParentOpeningTagEndMarker(child.firstChild) ? -printOpeningTagEndMarker(child).length : 0), child.endSourceSpan.start.offset + (child.lastChild && needsToBorrowParentClosingTagStartMarker(child.lastChild) ? printClosingTagStartMarker(child, options).length : needsToBorrowLastChildClosingTagEndMarker(child) ? -printClosingTagEndMarker(child.lastChild, options).length : 0) ), literalline ), printClosingTag(child, options), printClosingTagSuffix(child, options) ) ); } return print(childPath); } function printBetweenLine(prevNode, nextNode) { return isTextLikeNode(prevNode) && isTextLikeNode(nextNode) ? prevNode.isTrailingSpaceSensitive ? prevNode.hasTrailingSpaces ? preferHardlineAsLeadingSpaces(nextNode) ? hardline : line : "" : preferHardlineAsLeadingSpaces(nextNode) ? hardline : softline : (needsToBorrowNextOpeningTagStartMarker(prevNode) && /** * 123 */ (nextNode.firstChild || /** * 123 */ nextNode.isSelfClosing || /** * 123123 */ (prevNode.type === "element" && prevNode.isSelfClosing && needsToBorrowPrevClosingTagEndMarker(nextNode)) ? "" : !nextNode.isLeadingSpaceSensitive || preferHardlineAsLeadingSpaces(nextNode) || /** * Want to write us a letter? Use ourmailing address. */ (needsToBorrowPrevClosingTagEndMarker(nextNode) && prevNode.lastChild && needsToBorrowParentClosingTagStartMarker(prevNode.lastChild) && prevNode.lastChild.lastChild && needsToBorrowParentClosingTagStartMarker( prevNode.lastChild.lastChild )) ? hardline : nextNode.hasLeadingSpaces ? line : softline; } } function printOpeningTag(path, options, print) { const node = path.getValue(); const forceNotToBreakAttrContent = node.type === "element" && node.fullName === "script" && node.attrs.length === 1 && node.attrs[0].fullName === "src" && node.children.length === 0; return concat([ printOpeningTagStart(node, options), !node.attrs || node.attrs.length === 0 ? node.isSelfClosing ? /** *
* ^ */ " " : "" : concat([ indent( concat([ forceNotToBreakAttrContent ? " " : line, join( line, (ignoreAttributeData => { const hasPrettierIgnoreAttribute = typeof ignoreAttributeData === "boolean" ? () => ignoreAttributeData : Array.isArray(ignoreAttributeData) ? attr => ignoreAttributeData.indexOf(attr.rawName) !== -1 : () => false; return path.map(attrPath => { const attr = attrPath.getValue(); return hasPrettierIgnoreAttribute(attr) ? concat( replaceEndOfLineWith( options.originalText.slice( options.locStart(attr), options.locEnd(attr) ), literalline ) ) : print(attrPath); }, "attrs"); })( node.prev && node.prev.type === "comment" && getPrettierIgnoreAttributeCommentData(node.prev.value) ) ) ]) ), /** * 123456 */ (node.firstChild && needsToBorrowParentOpeningTagEndMarker(node.firstChild)) || /** * 123 */ (node.isSelfClosing && needsToBorrowLastChildClosingTagEndMarker(node.parent)) ? "" : node.isSelfClosing ? forceNotToBreakAttrContent ? " " : line : forceNotToBreakAttrContent ? "" : softline ]), node.isSelfClosing ? "" : printOpeningTagEnd(node) ]); } function printOpeningTagStart(node, options) { return node.prev && needsToBorrowNextOpeningTagStartMarker(node.prev) ? "" : concat([ printOpeningTagPrefix(node, options), printOpeningTagStartMarker(node) ]); } function printOpeningTagEnd(node) { return node.firstChild && needsToBorrowParentOpeningTagEndMarker(node.firstChild) ? "" : printOpeningTagEndMarker(node); } function printClosingTag(node, options) { return concat([ node.isSelfClosing ? "" : printClosingTagStart(node, options), printClosingTagEnd(node, options) ]); } function printClosingTagStart(node, options) { return node.lastChild && needsToBorrowParentClosingTagStartMarker(node.lastChild) ? "" : concat([ printClosingTagPrefix(node, options), printClosingTagStartMarker(node, options) ]); } function printClosingTagEnd(node, options) { return (node.next ? needsToBorrowPrevClosingTagEndMarker(node.next) : needsToBorrowLastChildClosingTagEndMarker(node.parent)) ? "" : concat([ printClosingTagEndMarker(node, options), printClosingTagSuffix(node, options) ]); } function needsToBorrowNextOpeningTagStartMarker(node) { /** * 123

*/ return ( node.next && !isTextLikeNode(node.next) && isTextLikeNode(node) && node.isTrailingSpaceSensitive && !node.hasTrailingSpaces ); } function needsToBorrowParentOpeningTagEndMarker(node) { /** *

123 * ^ * *

123 * ^ * *

*/ return ( node.lastChild && node.lastChild.isTrailingSpaceSensitive && !node.lastChild.hasTrailingSpaces && !isTextLikeNode(getLastDescendant(node.lastChild)) ); } function needsToBorrowParentClosingTagStartMarker(node) { /** *

* 123

* * 123
*/ return ( !node.next && !node.hasTrailingSpaces && node.isTrailingSpaceSensitive && isTextLikeNode(getLastDescendant(node)) ); } function printOpeningTagPrefix(node, options) { return needsToBorrowParentOpeningTagEndMarker(node) ? printOpeningTagEndMarker(node.parent) : needsToBorrowPrevClosingTagEndMarker(node) ? printClosingTagEndMarker(node.prev, options) : ""; } function printClosingTagPrefix(node, options) { return needsToBorrowLastChildClosingTagEndMarker(node) ? printClosingTagEndMarker(node.lastChild, options) : ""; } function printClosingTagSuffix(node, options) { return needsToBorrowParentClosingTagStartMarker(node) ? printClosingTagStartMarker(node.parent, options) : needsToBorrowNextOpeningTagStartMarker(node) ? printOpeningTagStartMarker(node.next) : ""; } function printOpeningTagStartMarker(node) { switch (node.type) { case "ieConditionalComment": case "ieConditionalStartComment": return `<${node.rawName}`; } // fall through default: return `<${node.rawName}`; } } function printOpeningTagEndMarker(node) { assert(!node.isSelfClosing); switch (node.type) { case "ieConditionalComment": return "]>"; case "element": if (node.condition) { return `>`; } // fall through default: return `>`; } } function printClosingTagStartMarker(node, options) { assert(!node.isSelfClosing); if (shouldNotPrintClosingTag(node, options)) { return ""; } switch (node.type) { case "ieConditionalComment": return "`; case "ieConditionalStartComment": return `]>`; case "interpolation": return "}}"; case "element": if (node.isSelfClosing) { return "/>"; } // fall through default: return ">"; } } function getTextValueParts(node, value = node.value) { return node.parent.isWhitespaceSensitive ? node.parent.isIndentationSensitive ? replaceEndOfLineWith(value, literalline) : replaceEndOfLineWith( dedentString(value.replace(/^\s*?\n|\n\s*?$/g, "")), hardline ) : // non-breaking whitespace: 0xA0 join(line, value.split(/[^\S\xA0]+/)).parts; } function printEmbeddedAttributeValue(node, originalTextToDoc, options) { const isKeyMatched = patterns => new RegExp(patterns.join("|")).test(node.fullName); const getValue = () => unescapeQuoteEntities(node.value); let shouldHug = false; const __onHtmlBindingRoot = root => { const rootNode = root.type === "NGRoot" ? root.node.type === "NGMicrosyntax" && root.node.body.length === 1 && root.node.body[0].type === "NGMicrosyntaxExpression" ? root.node.body[0].expression : root.node : root.type === "JsExpressionRoot" ? root.node : root; if ( rootNode && (rootNode.type === "ObjectExpression" || rootNode.type === "ArrayExpression") ) { shouldHug = true; } }; const printHug = doc => group(doc); const printExpand = doc => group(concat([indent(concat([softline, doc])), softline])); const printMaybeHug = doc => (shouldHug ? printHug(doc) : printExpand(doc)); const textToDoc = (code, opts) => originalTextToDoc(code, Object.assign({ __onHtmlBindingRoot }, opts)); if ( node.fullName === "srcset" && (node.parent.fullName === "img" || node.parent.fullName === "source") ) { return printExpand(printImgSrcset(getValue())); } if (options.parser === "vue") { if (node.fullName === "v-for") { return printVueFor(getValue(), textToDoc); } if (node.fullName === "slot-scope") { return printVueSlotScope(getValue(), textToDoc); } /** * @click="jsStatement" * @click="jsExpression" * v-on:click="jsStatement" * v-on:click="jsExpression" */ const vueEventBindingPatterns = ["^@", "^v-on:"]; /** * :class="vueExpression" * v-bind:id="vueExpression" */ const vueExpressionBindingPatterns = ["^:", "^v-bind:"]; /** * v-if="jsExpression" */ const jsExpressionBindingPatterns = ["^v-"]; if (isKeyMatched(vueEventBindingPatterns)) { const value = getValue(); return printMaybeHug( isVueEventBindingExpression(value) ? textToDoc(value, { parser: "__js_expression" }) : stripTrailingHardline( textToDoc(value, { parser: "__vue_event_binding" }) ) ); } if (isKeyMatched(vueExpressionBindingPatterns)) { return printMaybeHug( textToDoc(getValue(), { parser: "__vue_expression" }) ); } if (isKeyMatched(jsExpressionBindingPatterns)) { return printMaybeHug( textToDoc(getValue(), { parser: "__js_expression" }) ); } } if (options.parser === "angular") { const ngTextToDoc = (code, opts) => // angular does not allow trailing comma textToDoc(code, Object.assign({ trailingComma: "none" }, opts)); /** * *directive="angularDirective" */ const ngDirectiveBindingPatterns = ["^\\*"]; /** * (click)="angularStatement" * on-click="angularStatement" */ const ngStatementBindingPatterns = ["^\\(.+\\)$", "^on-"]; /** * [target]="angularExpression" * bind-target="angularExpression" * [(target)]="angularExpression" * bindon-target="angularExpression" */ const ngExpressionBindingPatterns = ["^\\[.+\\]$", "^bind(on)?-"]; if (isKeyMatched(ngStatementBindingPatterns)) { return printMaybeHug(ngTextToDoc(getValue(), { parser: "__ng_action" })); } if (isKeyMatched(ngExpressionBindingPatterns)) { return printMaybeHug(ngTextToDoc(getValue(), { parser: "__ng_binding" })); } if (isKeyMatched(ngDirectiveBindingPatterns)) { return printMaybeHug( ngTextToDoc(getValue(), { parser: "__ng_directive" }) ); } const interpolationRegex = /\{\{([\s\S]+?)\}\}/g; const value = getValue(); if (interpolationRegex.test(value)) { const parts = []; value.split(interpolationRegex).forEach((part, index) => { if (index % 2 === 0) { parts.push(concat(replaceEndOfLineWith(part, literalline))); } else { try { parts.push( group( concat([ "{{", indent( concat([ line, ngTextToDoc(part, { parser: "__ng_interpolation" }) ]) ), line, "}}" ]) ) ); } catch (e) { parts.push( "{{", concat(replaceEndOfLineWith(part, literalline)), "}}" ); } } }); return group(concat(parts)); } } return null; } module.exports = { preprocess, print: genericPrint, insertPragma, massageAstNode: clean, embed };