"use strict"; const clean = require("./clean"); const embed = require("./embed"); const { insertPragma } = require("./pragma"); const { printNumber, printString, hasIgnoreComment, hasNewline } = require("../common/util"); const { isNextLineEmpty } = require("../common/util-shared"); const { builders: { concat, join, line, hardline, softline, group, fill, indent, dedent, ifBreak }, utils: { removeLines } } = require("../doc"); const { getAncestorNode, getPropOfDeclNode, maybeToLowerCase, insideValueFunctionNode, insideICSSRuleNode, insideAtRuleNode, insideURLFunctionInImportAtRuleNode, isKeyframeAtRuleKeywords, isHTMLTag, isWideKeywords, isSCSS, isLastNode, isSCSSControlDirectiveNode, isDetachedRulesetDeclarationNode, isRelationalOperatorNode, isEqualityOperatorNode, isMultiplicationNode, isDivisionNode, isAdditionNode, isSubtractionNode, isMathOperatorNode, isEachKeywordNode, isForKeywordNode, isURLFunctionNode, isIfElseKeywordNode, hasComposesNode, hasParensAroundNode, hasEmptyRawBefore, isKeyValuePairNode, isDetachedRulesetCallNode, isTemplatePlaceholderNode, isTemplatePropNode, isPostcssSimpleVarNode, isSCSSMapItemNode, isInlineValueCommentNode, isHashNode, isLeftCurlyBraceNode, isRightCurlyBraceNode, isWordNode, isColonNode, isMediaAndSupportsKeywords, isColorAdjusterFuncNode } = require("./utils"); 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 "yaml": case "toml": return concat([node.raw, 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 // there's a bug in the less parser that trailing `\r`s are included in inline comments .replace(/^(\/\/[^]+)\r+$/, "$1") ); } 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": { const parentNode = path.getParentNode(); return concat([ node.raws.before.replace(/[\s;]/g, ""), insideICSSRuleNode(path) ? node.prop : maybeToLowerCase(node.prop), node.raws.between.trim() === ":" ? ":" : node.raws.between.trim(), node.extend ? "" : " ", hasComposesNode(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, "}" ]) : isTemplatePropNode(node) && !parentNode.raws.semicolon ? "" : ";" ]); } case "css-atrule": { const parentNode = path.getParentNode(); 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) ? "" : isTemplatePlaceholderNode(node) ? node.raws.afterName : " ", path.call(print, "params") ]) : "", node.selector ? indent(concat([" ", path.call(print, "selector")])) : "", node.value ? group( concat([ " ", path.call(print, "value"), isSCSSControlDirectiveNode(node) ? hasParensAroundNode(node) ? " " : line : "" ]) ) : node.name === "else" ? " " : "", node.nodes ? concat([ isSCSSControlDirectiveNode(node) ? "" : " ", "{", indent( concat([ node.nodes.length > 0 ? softline : "", printNodeSequence(path, options, print) ]) ), softline, "}" ]) : isTemplatePlaceholderNode(node) && !parentNode.raws.semicolon ? "" : ";" ]); } // 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": { return group( concat([ insideAtRuleNode(path, "custom-selector") ? concat([getAncestorNode(path, "css-atrule").customSelector, line]) : "", join( concat([ ",", insideAtRuleNode(path, ["extend", "custom-selector", "nest"]) ? line : hardline ]), path.map(print, "nodes") ) ]) ); } case "selector-selector": { return group(indent(concat(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(), "|"]) : "", node.value ]); } 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-value": 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 parentParentNode = path.getParentNode(1); 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 insideSCSSInterpolationInString = false; 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 spaces before/after string interpolation (i.e. `"#{my-fn("_")}"`) const isStartSCSSinterpolationInString = iNode.type === "value-string" && iNode.value.startsWith("#{"); const isEndingSCSSinterpolationInString = insideSCSSInterpolationInString && iNextNode.type === "value-string" && iNextNode.value.endsWith("}"); if ( isStartSCSSinterpolationInString || isEndingSCSSinterpolationInString ) { insideSCSSInterpolationInString = !insideSCSSInterpolationInString; continue; } if (insideSCSSInterpolationInString) { continue; } // Ignore colon (i.e. `:`) if (isColonNode(iNode) || isColonNode(iNextNode)) { 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; } // Ignore spaces after `#` and after `{` and before `}` in SCSS interpolation (i.e. `#{variable}`) if ( isHashNode(iNode) || isLeftCurlyBraceNode(iNode) || isRightCurlyBraceNode(iNextNode) || (isLeftCurlyBraceNode(iNextNode) && hasEmptyRawBefore(iNextNode)) || (isRightCurlyBraceNode(iNode) && hasEmptyRawBefore(iNextNode)) ) { continue; } // Ignore css variables and interpolation in SCSS (i.e. `--#{$var}`) if (iNode.value === "--" && isHashNode(iNextNode)) { continue; } // Formatting math operations const isMathOperator = isMathOperatorNode(iNode); const isNextMathOperator = isMathOperatorNode(iNextNode); // Print spaces before and after math operators beside SCSS interpolation as is // (i.e. `#{$var}+5`, `#{$var} +5`, `#{$var}+ 5`, `#{$var} + 5`) // (i.e. `5+#{$var}`, `5 +#{$var}`, `5+ #{$var}`, `5 + #{$var}`) if ( ((isMathOperator && isHashNode(iNextNode)) || (isNextMathOperator && isRightCurlyBraceNode(iNode))) && hasEmptyRawBefore(iNextNode) ) { continue; } // Print spaces before and after addition and subtraction math operators as is in `calc` function // due to the fact that it is not valid syntax // (i.e. `calc(1px+1px)`, `calc(1px+ 1px)`, `calc(1px +1px)`, `calc(1px + 1px)`) if ( insideValueFunctionNode(path, "calc") && (isAdditionNode(iNode) || isAdditionNode(iNextNode) || isSubtractionNode(iNode) || isSubtractionNode(iNextNode)) && hasEmptyRawBefore(iNextNode) ) { continue; } // Print spaces after `+` and `-` in color adjuster functions as is (e.g. `color(red l(+ 20%))`) // Adjusters with signed numbers (e.g. `color(red l(+20%))`) output as-is. const isColorAdjusterNode = (isAdditionNode(iNode) || isSubtractionNode(iNode)) && i === 0 && (iNextNode.type === "value-number" || iNextNode.isHex) && (parentParentNode && isColorAdjusterFuncNode(parentParentNode)) && !hasEmptyRawBefore(iNextNode); const requireSpaceBeforeOperator = (iNextNextNode && iNextNextNode.type === "value-func") || (iNextNextNode && isWordNode(iNextNextNode)) || iNode.type === "value-func" || isWordNode(iNode); const requireSpaceAfterOperator = iNextNode.type === "value-func" || isWordNode(iNextNode) || (iPrevNode && iPrevNode.type === "value-func") || (iPrevNode && isWordNode(iPrevNode)); // Formatting `/`, `+`, `-` sign if ( !(isMultiplicationNode(iNextNode) || isMultiplicationNode(iNode)) && !insideValueFunctionNode(path, "calc") && !isColorAdjusterNode && ((isDivisionNode(iNextNode) && !requireSpaceBeforeOperator) || (isDivisionNode(iNode) && !requireSpaceAfterOperator) || (isAdditionNode(iNextNode) && !requireSpaceBeforeOperator) || (isAdditionNode(iNode) && !requireSpaceAfterOperator) || isSubtractionNode(iNextNode) || isSubtractionNode(iNode)) && (hasEmptyRawBefore(iNextNode) || (isMathOperator && (!iPrevNode || (iPrevNode && isMathOperatorNode(iPrevNode))))) ) { continue; } // Ignore inline comment, they already contain newline at end (i.e. `// Comment`) // Add `hardline` after inline comment (i.e. `// comment\n foo: bar;`) const isInlineComment = isInlineValueCommentNode(iNode); if ( (iPrevNode && isInlineValueCommentNode(iPrevNode)) || isInlineComment || isInlineValueCommentNode(iNextNode) ) { if (isInlineComment) { parts.push(hardline); } continue; } // Handle keywords in SCSS control directive if ( isControlDirective && (isEqualityOperatorNode(iNextNode) || isRelationalOperatorNode(iNextNode) || isIfElseKeywordNode(iNextNode) || isEachKeywordNode(iNode) || isForKeywordNode(iNode)) ) { parts.push(" "); continue; } // At-rule `namespace` should be in one line if ( atRuleAncestorNode && atRuleAncestorNode.name.toLowerCase() === "namespace" ) { parts.push(" "); continue; } // Formatting `grid` property if (isGridValue) { if (iNode.source.start.line !== iNextNode.source.start.line) { parts.push(hardline); didBreak = true; } else { parts.push(" "); } continue; } // Add `space` before next math operation // Note: `grip` property have `/` delimiter and it is not math operation, so // `grid` property handles above if (isNextMathOperator) { parts.push(" "); continue; } // Be default all values go through `line` 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-func": { return concat([ node.value, insideAtRuleNode(path, "supports") && isMediaAndSupportsKeywords(node) ? " " : "", path.call(print, "group") ]); } case "value-paren": { return node.value; } case "value-number": { return concat([printCssNumber(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, // Don't add spaces on `:` in `url` function (i.e. `url(fbglyph: cross-outline, fig-white)`) insideValueFunctionNode(path, "url") ? "" : line ]); } case "value-comma": { return concat([node.value, " "]); } case "value-string": { return 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" && !hasNewline( options.originalText, options.locStart(node.nodes[i + 1]), { backwards: true } ) && node.nodes[i].type !== "yaml" && node.nodes[i].type !== "toml") || (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 ( isNextLineEmpty( options.originalText, pathChild.getValue(), options ) && node.nodes[i].type !== "yaml" && node.nodes[i].type !== "toml" ) { 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 => 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 || "") + printCssNumber(number) + maybeToLowerCase(unit || "") : match ); } function printCssNumber(rawNumber) { return ( printNumber(rawNumber) // Remove trailing `.0`. .replace(/\.0(?=$|e)/, "") ); } module.exports = { print: genericPrint, embed, insertPragma, hasPrettierIgnore: hasIgnoreComment, massageAstNode: clean };