"use strict"; const clean = require("./clean"); const privateUtil = require("../common/util"); const sharedUtil = require("../common/util-shared"); const doc = require("../doc"); const docBuilders = doc.builders; const concat = docBuilders.concat; const join = docBuilders.join; const line = docBuilders.line; const hardline = docBuilders.hardline; const softline = docBuilders.softline; const group = docBuilders.group; const fill = docBuilders.fill; const indent = docBuilders.indent; const dedent = docBuilders.dedent; const ifBreak = docBuilders.ifBreak; const docUtils = doc.utils; const removeLines = docUtils.removeLines; const utils = require("./utils"); const getAncestorNode = utils.getAncestorNode; const getPropOfDeclNode = utils.getPropOfDeclNode; const maybeToLowerCase = utils.maybeToLowerCase; const insideValueFunctionNode = utils.insideValueFunctionNode; const insideICSSRuleNode = utils.insideICSSRuleNode; const insideAtRuleNode = utils.insideAtRuleNode; const insideURLFunctionInImportAtRuleNode = utils.insideURLFunctionInImportAtRuleNode; const isKeyframeAtRuleKeywords = utils.isKeyframeAtRuleKeywords; const isHTMLTag = utils.isHTMLTag; const isWideKeywords = utils.isWideKeywords; const isSCSS = utils.isSCSS; const isLastNode = utils.isLastNode; const isSCSSControlDirectiveNode = utils.isSCSSControlDirectiveNode; const isDetachedRulesetDeclarationNode = utils.isDetachedRulesetDeclarationNode; const isRelationalOperatorNode = utils.isRelationalOperatorNode; const isEqualityOperatorNode = utils.isEqualityOperatorNode; const isMathOperatorNode = utils.isMathOperatorNode; const isEachKeywordNode = utils.isEachKeywordNode; const isParenGroupNode = utils.isParenGroupNode; const isForKeywordNode = utils.isForKeywordNode; const isURLFunctionNode = utils.isURLFunctionNode; const isIfElseKeywordNode = utils.isIfElseKeywordNode; const hasLessExtendValueNode = utils.hasLessExtendValueNode; const hasComposesValueNode = utils.hasComposesValueNode; const hasParensAroundValueNode = utils.hasParensAroundValueNode; const isKeyValuePairNode = utils.isKeyValuePairNode; const isDetachedRulesetCallNode = utils.isDetachedRulesetCallNode; const isPostcssSimpleVarNode = utils.isPostcssSimpleVarNode; const isSCSSMapItemNode = utils.isSCSSMapItemNode; function shouldPrintComma(options) { switch (options.trailingComma) { case "all": case "es5": return true; case "none": default: return false; } } function genericPrint(path, options, print) { const node = path.getValue(); /* istanbul ignore if */ if (!node) { return ""; } if (typeof node === "string") { return node; } switch (node.type) { case "frontmatter": return concat([node.value, hardline]); case "css-root": { const nodes = printNodeSequence(path, options, print); if (nodes.parts.length) { return concat([nodes, hardline]); } return nodes; } case "css-comment": { if (node.raws.content) { return node.raws.content; } const text = options.originalText.slice( options.locStart(node), options.locEnd(node) ); const rawText = node.raws.text || node.text; // Workaround a bug where the location is off. // https://github.com/postcss/postcss-scss/issues/63 if (text.indexOf(rawText) === -1) { if (node.raws.inline) { return concat(["// ", rawText]); } return concat(["/* ", rawText, " */"]); } return text; } case "css-rule": { return concat([ path.call(print, "selector"), node.important ? " !important" : "", node.nodes ? concat([ " {", node.nodes.length > 0 ? indent( concat([hardline, printNodeSequence(path, options, print)]) ) : "", hardline, "}", isDetachedRulesetDeclarationNode(node) ? ";" : "" ]) : ";" ]); } case "css-decl": { return concat([ node.raws.before.replace(/[\s;]/g, ""), insideICSSRuleNode(path) ? node.prop : maybeToLowerCase(node.prop), node.raws.between.trim() === ":" ? ":" : node.raws.between.trim(), // When the following less construct &:extend(.foo); is parsed with scss, // it will put a space after `:` and break it. Ideally we should parse // less files with less, but we can hardcode this to work with scss as // well. hasLessExtendValueNode(node) ? "" : " ", hasComposesValueNode(node) ? removeLines(path.call(print, "value")) : path.call(print, "value"), node.raws.important ? node.raws.important.replace(/\s*!\s*important/i, " !important") : node.important ? " !important" : "", node.raws.scssDefault ? node.raws.scssDefault.replace(/\s*!default/i, " !default") : node.scssDefault ? " !default" : "", node.raws.scssGlobal ? node.raws.scssGlobal.replace(/\s*!global/i, " !global") : node.scssGlobal ? " !global" : "", node.nodes ? concat([ " {", indent( concat([softline, printNodeSequence(path, options, print)]) ), softline, "}" ]) : ";" ]); } case "css-atrule": { return concat([ "@", // If a Less file ends up being parsed with the SCSS parser, Less // variable declarations will be parsed as at-rules with names ending // with a colon, so keep the original case then. isDetachedRulesetCallNode(node) || node.name.endsWith(":") ? node.name : maybeToLowerCase(node.name), node.params ? concat([ isDetachedRulesetCallNode(node) ? "" : " ", path.call(print, "params") ]) : "", node.selector ? indent(concat([" ", path.call(print, "selector")])) : "", node.value ? group( concat([ " ", path.call(print, "value"), isSCSSControlDirectiveNode(node) ? hasParensAroundValueNode(node) ? " " : line : "" ]) ) : node.name === "else" ? " " : "", node.nodes ? concat([ isSCSSControlDirectiveNode(node) ? "" : " ", "{", indent( concat([ node.nodes.length > 0 ? softline : "", printNodeSequence(path, options, print) ]) ), softline, "}" ]) : ";" ]); } // postcss-media-query-parser case "media-query-list": { const parts = []; path.each(childPath => { const node = childPath.getValue(); if (node.type === "media-query" && node.value === "") { return; } parts.push(childPath.call(print)); }, "nodes"); return group(indent(join(line, parts))); } case "media-query": { return concat([ join(" ", path.map(print, "nodes")), isLastNode(path, node) ? "" : "," ]); } case "media-type": { return adjustNumbers(adjustStrings(node.value, options)); } case "media-feature-expression": { if (!node.nodes) { return node.value; } return concat(["(", concat(path.map(print, "nodes")), ")"]); } case "media-feature": { return maybeToLowerCase( adjustStrings(node.value.replace(/ +/g, " "), options) ); } case "media-colon": { return concat([node.value, " "]); } case "media-value": { return adjustNumbers(adjustStrings(node.value, options)); } case "media-keyword": { return adjustStrings(node.value, options); } case "media-url": { return adjustStrings( node.value.replace(/^url\(\s+/gi, "url(").replace(/\s+\)$/gi, ")"), options ); } case "media-unknown": { return node.value; } // postcss-selector-parser case "selector-root": { const atRuleAncestorNode = getAncestorNode(path, "css-atrule"); const insideAtRuleNode = atRuleAncestorNode && ["extend", "custom-selector", "nest"].indexOf( atRuleAncestorNode.name ) !== -1; return group( concat([ atRuleAncestorNode && atRuleAncestorNode.name === "custom-selector" ? concat([atRuleAncestorNode.customSelector, line]) : "", join( concat([",", insideAtRuleNode ? line : hardline]), path.map(print, "nodes") ) ]) ); } case "selector-comment": { return node.value; } case "selector-string": { return adjustStrings(node.value, options); } case "selector-tag": { const parentNode = path.getParentNode(); const index = parentNode && parentNode.nodes.indexOf(node); const prevNode = index && parentNode.nodes[index - 1]; return concat([ node.namespace ? concat([node.namespace === true ? "" : node.namespace.trim(), "|"]) : "", prevNode.type === "selector-nesting" ? node.value : adjustNumbers( isHTMLTag(node.value) || isKeyframeAtRuleKeywords(path, node.value) ? node.value.toLowerCase() : node.value ) ]); } case "selector-id": { return concat(["#", node.value]); } case "selector-class": { return concat([".", adjustNumbers(adjustStrings(node.value, options))]); } case "selector-attribute": { return concat([ "[", node.namespace ? concat([node.namespace === true ? "" : node.namespace.trim(), "|"]) : "", node.attribute.trim(), node.operator ? node.operator : "", node.value ? quoteAttributeValue( adjustStrings(node.value.trim(), options), options ) : "", node.insensitive ? " i" : "", "]" ]); } case "selector-combinator": { if ( node.value === "+" || node.value === ">" || node.value === "~" || node.value === ">>>" ) { const parentNode = path.getParentNode(); const leading = parentNode.type === "selector-selector" && parentNode.nodes[0] === node ? "" : line; return concat([leading, node.value, isLastNode(path, node) ? "" : " "]); } const leading = node.value.trim().startsWith("(") ? line : ""; const value = adjustNumbers(adjustStrings(node.value.trim(), options)) || line; return concat([leading, value]); } case "selector-universal": { return concat([ node.namespace ? concat([node.namespace === true ? "" : node.namespace.trim(), "|"]) : "", adjustNumbers(node.value) ]); } case "selector-selector": { return group(indent(concat(path.map(print, "nodes")))); } case "selector-pseudo": { return concat([ maybeToLowerCase(node.value), node.nodes && node.nodes.length > 0 ? concat(["(", join(", ", path.map(print, "nodes")), ")"]) : "" ]); } case "selector-nesting": { return node.value; } case "selector-unknown": { const ruleAncestorNode = getAncestorNode(path, "css-rule"); // Nested SCSS property if (ruleAncestorNode && ruleAncestorNode.isSCSSNesterProperty) { return adjustNumbers( adjustStrings(maybeToLowerCase(node.value), options) ); } return node.value; } // postcss-values-parser case "value-root": { return path.call(print, "group"); } case "value-comment": { return concat([ node.inline ? "//" : "/*", node.value, node.inline ? "" : "*/" ]); } case "value-comma_group": { const parentNode = path.getParentNode(); const declAncestorProp = getPropOfDeclNode(path); const isGridValue = declAncestorProp && parentNode.type === "value-value" && (declAncestorProp === "grid" || declAncestorProp.startsWith("grid-template")); const atRuleAncestorNode = getAncestorNode(path, "css-atrule"); const isControlDirective = atRuleAncestorNode && isSCSSControlDirectiveNode(atRuleAncestorNode); const printed = path.map(print, "groups"); const parts = []; const insideURLFunction = insideValueFunctionNode(path, "url"); let didBreak = false; for (let i = 0; i < node.groups.length; ++i) { parts.push(printed[i]); // Ignore value inside `url()` if (insideURLFunction) { continue; } const iPrevNode = node.groups[i - 1]; const iNode = node.groups[i]; const iNextNode = node.groups[i + 1]; const iNextNextNode = node.groups[i + 2]; // Ignore after latest node (i.e. before semicolon) if (!iNextNode) { continue; } // Ignore colon if (iNode.value === ":") { continue; } // Ignore `@` in Less (i.e. `@@var;`) if (iNode.type === "value-atword" && iNode.value === "") { continue; } // Ignore `~` in Less (i.e. `content: ~"^//* some horrible but needed css hack";`) if (iNode.value === "~") { continue; } // Ignore `\` (i.e. `$variable: \@small;`) if (iNode.value === "\\") { continue; } // Ignore `$$` (i.e. `background-color: $$(style)Color;`) if (isPostcssSimpleVarNode(iNode, iNextNode)) { continue; } if ( (iPrevNode && iPrevNode.type === "value-comment" && iPrevNode.inline) || (iNextNode.type === "value-comment" && iNextNode.inline) ) { continue; } const isHash = iNode.type === "value-word" && iNode.value === "#"; const isLeftCurlyBrace = iNode.type === "value-word" && iNode.value === "{"; const isNextLeftCurlyBrace = iNextNode.type === "value-word" && iNextNode.value === "{"; const isRightCurlyBrace = iNode.type === "value-word" && iNode.value === "}"; const isNextRightCurlyBrace = iNextNode.type === "value-word" && iNextNode.value === "}"; // Ignore interpolation in SCSS (i.e. `#{variable}`) if ( isHash || isLeftCurlyBrace || isNextRightCurlyBrace || (isNextLeftCurlyBrace && iNextNode.raws && iNextNode.raws.before === "") || (isRightCurlyBrace && iNextNode.raws && iNextNode.raws.before === "") ) { continue; } // Ignore css variables and interpolation in SCSS (i.e. `--#{$var}`) if (iNode.value === "--" && iNextNode.value === "#") { continue; } const isNextHash = iNextNode.type === "value-word" && iNextNode.value === "#"; const isMathOperator = isMathOperatorNode(iNode); const isNextMathOperator = isMathOperatorNode(iNextNode); const isMultiplication = !isNextHash && isMathOperator && iNode.value === "*"; const isNextMultiplication = !isRightCurlyBrace && isNextMathOperator && iNextNode.value === "*"; const isDivision = !isNextHash && isMathOperator && iNode.value === "/"; const isNextDivision = !isRightCurlyBrace && isNextMathOperator && iNextNode.value === "/"; const isAddition = !isNextHash && isMathOperator && iNode.value === "+"; const isNextAddition = !isRightCurlyBrace && isNextMathOperator && iNextNode.value === "+"; const isPrevFunction = iPrevNode && iPrevNode.type === "value-func"; const isFunction = iNode.type === "value-func"; const isNextFunction = iNextNode.type === "value-func"; const isNextNextFunction = iNextNextNode && iNextNextNode.type === "value-func"; const isPrevWord = iPrevNode && ["value-word", "value-atword"].indexOf(iPrevNode.type) !== -1; const isWord = ["value-word", "value-atword"].indexOf(iNode.type) !== -1; const isNextWord = ["value-word", "value-atword"].indexOf(iNextNode.type) !== -1; const isNextNextWord = iNextNextNode && ["value-word", "value-atword"].indexOf(iNextNextNode.type) !== -1; // Math operators const insideCalcFunction = insideValueFunctionNode(path, "calc"); const hasSpaceBeforeOperator = isNextNextFunction || isNextNextWord || isFunction || isWord; const hasSpaceAfterOperator = isNextFunction || isNextWord || isPrevFunction || isPrevWord; if ( (isMathOperator || isNextMathOperator) && // Multiplication !isMultiplication && !isNextMultiplication && // Division !(isNextDivision && (hasSpaceBeforeOperator || insideCalcFunction)) && !(isDivision && (hasSpaceAfterOperator || insideCalcFunction)) && // Addition !(isNextAddition && hasSpaceBeforeOperator) && !(isAddition && hasSpaceAfterOperator) ) { const isNextParenGroup = isParenGroupNode(iNextNode); const isNextValueNumber = iNextNode.type === "value-number"; if ( (iNextNode.raws && iNextNode.raws.before === "") || (isMathOperator && (isNextParenGroup || isNextWord || isNextValueNumber || isMathOperatorNode(iNextNode)) && (!iPrevNode || (iPrevNode && isMathOperatorNode(iPrevNode)))) ) { continue; } } const isEqualityOperator = isControlDirective && isEqualityOperatorNode(iNode); const isRelationalOperator = isControlDirective && isRelationalOperatorNode(iNode); const isNextEqualityOperator = isControlDirective && isEqualityOperatorNode(iNextNode); const isNextRelationalOperator = isControlDirective && isRelationalOperatorNode(iNextNode); const isNextIfElseKeyword = isControlDirective && isIfElseKeywordNode(iNextNode); const isEachKeyword = isControlDirective && isEachKeywordNode(iNode); const isNextEachKeyword = isControlDirective && isEachKeywordNode(iNextNode); const isForKeyword = atRuleAncestorNode && atRuleAncestorNode.name === "for" && isForKeywordNode(iNode); const isNextForKeyword = isControlDirective && isForKeywordNode(iNextNode); const IsNextColon = iNextNode.value === ":"; if (isGridValue) { if (iNode.source.start.line !== iNextNode.source.start.line) { parts.push(hardline); didBreak = true; } else { parts.push(" "); } } else if (iNode.type === "value-comment" && iNode.inline) { parts.push(hardline); } else if ( isNextMathOperator || isNextEqualityOperator || isNextRelationalOperator || isNextIfElseKeyword || isForKeyword || isEachKeyword || (atRuleAncestorNode && atRuleAncestorNode.name.toLowerCase() === "namespace") ) { parts.push(" "); } else if ( !IsNextColon || isEqualityOperator || isRelationalOperator || isNextForKeyword || isNextEachKeyword ) { parts.push(line); } } if (didBreak) { parts.unshift(hardline); } if (isControlDirective) { return group(indent(concat(parts))); } // Indent is not needed for import url when url is very long // and node has two groups // when type is value-comma_group // example @import url("verylongurl") projection,tv if (insideURLFunctionInImportAtRuleNode(path)) { return group(fill(parts)); } return group(indent(fill(parts))); } case "value-paren_group": { const parentNode = path.getParentNode(); if ( parentNode && isURLFunctionNode(parentNode) && (node.groups.length === 1 || (node.groups.length > 0 && node.groups[0].type === "value-comma_group" && node.groups[0].groups.length > 0 && node.groups[0].groups[0].type === "value-word" && node.groups[0].groups[0].value.startsWith("data:"))) ) { return concat([ node.open ? path.call(print, "open") : "", join(",", path.map(print, "groups")), node.close ? path.call(print, "close") : "" ]); } if (!node.open) { const printed = path.map(print, "groups"); const res = []; for (let i = 0; i < printed.length; i++) { if (i !== 0) { res.push(concat([",", line])); } res.push(printed[i]); } return group(indent(fill(res))); } const isSCSSMapItem = isSCSSMapItemNode(path); return group( concat([ node.open ? path.call(print, "open") : "", indent( concat([ softline, join( concat([",", line]), path.map(childPath => { const node = childPath.getValue(); const printed = print(childPath); // Key/Value pair in open paren already indented if ( isKeyValuePairNode(node) && node.type === "value-comma_group" && node.groups && node.groups[2] && node.groups[2].type === "value-paren_group" ) { printed.contents.contents.parts[1] = group( printed.contents.contents.parts[1] ); return group(dedent(printed)); } return printed; }, "groups") ) ]) ), ifBreak( isSCSS(options.parser, options.originalText) && isSCSSMapItem && shouldPrintComma(options) ? "," : "" ), softline, node.close ? path.call(print, "close") : "" ]), { shouldBreak: isSCSSMapItem } ); } case "value-value": { return path.call(print, "group"); } case "value-func": { const insideAtRuleSupportsNode = insideAtRuleNode(path, "supports"); const isKeyword = ["not", "and", "or"].indexOf(node.value.toLowerCase()) !== -1; return concat([ node.value, insideAtRuleSupportsNode && isKeyword ? " " : "", path.call(print, "group") ]); } case "value-paren": { return node.value; } case "value-number": { return concat([printNumber(node.value), maybeToLowerCase(node.unit)]); } case "value-operator": { return node.value; } case "value-word": { if ((node.isColor && node.isHex) || isWideKeywords(node.value)) { return node.value.toLowerCase(); } return node.value; } case "value-colon": { return concat([ node.value, insideValueFunctionNode(path, "url") ? "" : line ]); } case "value-comma": { return concat([node.value, " "]); } case "value-string": { return privateUtil.printString( node.raws.quote + node.value + node.raws.quote, options ); } case "value-atword": { return concat(["@", node.value]); } case "value-unicode-range": { return node.value; } case "value-unknown": { return node.value; } default: /* istanbul ignore next */ throw new Error(`Unknown postcss type ${JSON.stringify(node.type)}`); } } function printNodeSequence(path, options, print) { const node = path.getValue(); const parts = []; let i = 0; path.map(pathChild => { const prevNode = node.nodes[i - 1]; if ( prevNode && prevNode.type === "css-comment" && prevNode.text.trim() === "prettier-ignore" ) { const childNode = pathChild.getValue(); parts.push( options.originalText.slice( options.locStart(childNode), options.locEnd(childNode) ) ); } else { parts.push(pathChild.call(print)); } if (i !== node.nodes.length - 1) { if ( (node.nodes[i + 1].type === "css-comment" && !privateUtil.hasNewline( options.originalText, options.locStart(node.nodes[i + 1]), { backwards: true } )) || (node.nodes[i + 1].type === "css-atrule" && node.nodes[i + 1].name === "else" && node.nodes[i].type !== "css-comment") ) { parts.push(" "); } else { parts.push(hardline); if ( sharedUtil.isNextLineEmpty( options.originalText, pathChild.getValue(), options ) ) { parts.push(hardline); } } } i++; }, "nodes"); return concat(parts); } const STRING_REGEX = /(['"])(?:(?!\1)[^\\]|\\[\s\S])*\1/g; const NUMBER_REGEX = /(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/g; const STANDARD_UNIT_REGEX = /[a-zA-Z]+/g; const WORD_PART_REGEX = /[$@]?[a-zA-Z_\u0080-\uFFFF][\w\-\u0080-\uFFFF]*/g; const ADJUST_NUMBERS_REGEX = RegExp( STRING_REGEX.source + `|` + `(${WORD_PART_REGEX.source})?` + `(${NUMBER_REGEX.source})` + `(${STANDARD_UNIT_REGEX.source})?`, "g" ); function adjustStrings(value, options) { return value.replace(STRING_REGEX, match => privateUtil.printString(match, options) ); } function quoteAttributeValue(value, options) { const quote = options.singleQuote ? "'" : '"'; return value.includes('"') || value.includes("'") ? value : quote + value + quote; } function adjustNumbers(value) { return value.replace( ADJUST_NUMBERS_REGEX, (match, quote, wordPart, number, unit) => !wordPart && number ? (wordPart || "") + printNumber(number) + maybeToLowerCase(unit || "") : match ); } function printNumber(rawNumber) { return ( privateUtil .printNumber(rawNumber) // Remove trailing `.0`. .replace(/\.0(?=$|e)/, "") ); } module.exports = { print: genericPrint, hasPrettierIgnore: privateUtil.hasIgnoreComment, massageAstNode: clean };