584 lines
16 KiB
JavaScript
584 lines
16 KiB
JavaScript
import {
|
|
breakParent,
|
|
dedent,
|
|
fill,
|
|
group,
|
|
hardline,
|
|
indent,
|
|
line,
|
|
lineSuffix,
|
|
softline,
|
|
} from "../../document/index.js";
|
|
import { locEnd, locStart } from "../loc.js";
|
|
import {
|
|
getPropOfDeclNode,
|
|
hasEmptyRawBefore,
|
|
insideAtRuleNode,
|
|
insideURLFunctionInImportAtRuleNode,
|
|
insideValueFunctionNode,
|
|
isAdditionNode,
|
|
isAtWordPlaceholderNode,
|
|
isColonNode,
|
|
isColorAdjusterFuncNode,
|
|
isDivisionNode,
|
|
isEachKeywordNode,
|
|
isEqualityOperatorNode,
|
|
isForKeywordNode,
|
|
isHashNode,
|
|
isIfElseKeywordNode,
|
|
isInlineValueCommentNode,
|
|
isLeftCurlyBraceNode,
|
|
isMathOperatorNode,
|
|
isMultiplicationNode,
|
|
isParenGroupNode,
|
|
isPostcssSimpleVarNode,
|
|
isRelationalOperatorNode,
|
|
isRightCurlyBraceNode,
|
|
isSCSSControlDirectiveNode,
|
|
isSubtractionNode,
|
|
isWordNode,
|
|
} from "../utilities/index.js";
|
|
|
|
/**
|
|
* @import AstPath from "../../common/ast-path.js"
|
|
* @import {Doc} from "../../document/index.js"
|
|
*/
|
|
|
|
/**
|
|
* @param {AstPath} path
|
|
* @param {*} options
|
|
* @param {(...args: *[]) => Doc} print
|
|
* @returns {Doc}
|
|
*/
|
|
function printCommaSeparatedValueGroup(path, options, print) {
|
|
const { node } = path;
|
|
const parentNode = path.parent;
|
|
const parentParentNode = path.grandparent;
|
|
const declAncestorProp = getPropOfDeclNode(path);
|
|
const isGridValue =
|
|
declAncestorProp &&
|
|
parentNode.type === "value-value" &&
|
|
(declAncestorProp === "grid" ||
|
|
declAncestorProp.startsWith("grid-template"));
|
|
const atRuleAncestorNode = path.findAncestor(
|
|
(node) => node.type === "css-atrule",
|
|
);
|
|
const isControlDirective =
|
|
atRuleAncestorNode &&
|
|
isSCSSControlDirectiveNode(atRuleAncestorNode, options);
|
|
const hasInlineComment = node.groups.some((node) =>
|
|
isInlineValueCommentNode(node),
|
|
);
|
|
|
|
const printed = path.map(print, "groups");
|
|
/*
|
|
* We assume parts always meet following conditions:
|
|
* - parts.length is odd
|
|
* - odd (0-indexed) elements are line-like doc
|
|
* We can achieve this by following way:
|
|
* - if we push line-like doc, we push empty string after it
|
|
* - if we push non-line-like doc, push [parts.pop(), doc] instead
|
|
*/
|
|
/** @type {Doc[]} */
|
|
let parts = [""];
|
|
const insideURLFunction = insideValueFunctionNode(path, "url");
|
|
|
|
let insideSCSSInterpolationInString = false;
|
|
let didBreak = false;
|
|
|
|
for (let i = 0; i < node.groups.length; ++i) {
|
|
const iPrevNode = node.groups[i - 1];
|
|
const iNode = node.groups[i];
|
|
const iNextNode = node.groups[i + 1];
|
|
|
|
// If the node is comment and last node print it in a line suffix
|
|
if (isInlineValueCommentNode(iNode) && !iNextNode) {
|
|
// TODO: Improve this part
|
|
// This `lineSuffix` should be done in `value-comment` print
|
|
// But since we add `line` to groups in `value-paren_group`,
|
|
// The format result looks bad for now
|
|
parts.push([parts.pop(), lineSuffix([" ", printed[i]])]);
|
|
continue;
|
|
}
|
|
|
|
parts.push([parts.pop(), printed[i]]);
|
|
|
|
if (insideURLFunction) {
|
|
if ((iNextNode && isAdditionNode(iNextNode)) || isAdditionNode(iNode)) {
|
|
parts.push([parts.pop(), " "]);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Ignore SCSS @forward wildcard suffix
|
|
if (
|
|
insideAtRuleNode(path, "forward") &&
|
|
iNode.type === "value-word" &&
|
|
iNode.value &&
|
|
iPrevNode !== undefined &&
|
|
iPrevNode.type === "value-word" &&
|
|
iPrevNode.value === "as" &&
|
|
iNextNode.type === "value-operator" &&
|
|
iNextNode.value === "*"
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// Ignore Tailwind `@utility` directive ( https://tailwindcss.com/docs/adding-custom-styles#functional-utilities )
|
|
if (
|
|
insideAtRuleNode(path, "utility") &&
|
|
iNode.type === "value-word" &&
|
|
iNextNode &&
|
|
iNextNode.type === "value-operator" &&
|
|
iNextNode.value === "*"
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// Ignore after latest node (i.e. before semicolon)
|
|
if (!iNextNode) {
|
|
continue;
|
|
}
|
|
|
|
// Don't print a space before the `;` branch delimiter in the SCSS `if()`
|
|
// function (i.e. `if(condition: value; else: value)`)
|
|
if (
|
|
options.parser === "scss" &&
|
|
iNextNode.type === "value-word" &&
|
|
iNextNode.value === ";" &&
|
|
path.match(
|
|
undefined,
|
|
(node, key) => key === "groups" && node.type === "value-paren_group",
|
|
(node, key) =>
|
|
key === "group" && node.type === "value-func" && node.value === "if",
|
|
)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// We should keep spaces between words in a embedded JS expression
|
|
// examples:
|
|
// styled.div` font-size: var(--font-size-h${({ level }) => level}); `;
|
|
// styled.div` grid-area: area-${({ area }) => area}; `;
|
|
// styled.div` border: 1px ${solid} red; `;
|
|
if (
|
|
iNode.type === "value-word" &&
|
|
isAtWordPlaceholderNode(iNextNode) &&
|
|
locEnd(iNode) === locStart(iNextNode)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// Ignore spaces before/after string interpolation (i.e. `"#{my-fn("_")}"`)
|
|
if (iNode.type === "value-string" && iNode.quoted) {
|
|
const positionOfOpeningInterpolation = iNode.value.lastIndexOf("#{");
|
|
const positionOfClosingInterpolation = iNode.value.lastIndexOf("}");
|
|
if (
|
|
positionOfOpeningInterpolation !== -1 &&
|
|
positionOfClosingInterpolation !== -1
|
|
) {
|
|
insideSCSSInterpolationInString =
|
|
positionOfOpeningInterpolation > positionOfClosingInterpolation;
|
|
} else if (positionOfOpeningInterpolation !== -1) {
|
|
insideSCSSInterpolationInString = true;
|
|
} else if (positionOfClosingInterpolation !== -1) {
|
|
insideSCSSInterpolationInString = false;
|
|
}
|
|
}
|
|
|
|
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 === "" ||
|
|
/*
|
|
@var[ @notVarNested ][notVar]
|
|
^^^^^
|
|
*/
|
|
iNode.value.endsWith("["))
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
@var[ @notVarNested ][notVar]
|
|
^^^^^^^^^
|
|
*/
|
|
if (iNextNode.type === "value-word" && iNextNode.value.startsWith("]")) {
|
|
continue;
|
|
}
|
|
|
|
// Ignore `~` in Less (i.e. `content: ~"^//* some horrible but needed css hack";`)
|
|
if (iNode.value === "~") {
|
|
continue;
|
|
}
|
|
|
|
// Less property/variable lookup
|
|
// https://lesscss.org/features/#detached-rulesets-feature-property-variable-accessors
|
|
if (options.parser === "less") {
|
|
// `var [@result]`
|
|
// ^
|
|
if (iNextNode?.type === "value-word" && iNextNode.value === "[") {
|
|
continue;
|
|
}
|
|
|
|
// `var[ @result]`
|
|
// ^
|
|
// `@var [ @@foo ] [ bar ]`
|
|
// ^
|
|
if (
|
|
iNode.type === "value-word" &&
|
|
iNode.value === "[" &&
|
|
(iNextNode?.type === "value-atword" || iNextNode?.type === "value-word")
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// `@var [ @@foo ][ bar ]`
|
|
// ^^
|
|
if (
|
|
iNode.type === "value-word" &&
|
|
iNode.value === "][" &&
|
|
iNextNode?.type === "value-word"
|
|
) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Ignore escape `\`
|
|
if (
|
|
iNode.type !== "value-string" &&
|
|
iNode.value &&
|
|
iNode.value.includes("\\") &&
|
|
iNextNode &&
|
|
iNextNode.type !== "value-comment"
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// Ignore escaped `/`
|
|
if (
|
|
iPrevNode?.value &&
|
|
iPrevNode.value.indexOf("\\") === iPrevNode.value.length - 1 &&
|
|
iNode.type === "value-operator" &&
|
|
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;
|
|
}
|
|
|
|
// absolute paths are only parsed as one token if they are part of url(/abs/path) call
|
|
// but if you have custom -fb-url(/abs/path/) then it is parsed as "division /" and rest
|
|
// of the path. We don't want to put a space after that first division in this case.
|
|
if (!iPrevNode && isDivisionNode(iNode)) {
|
|
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;
|
|
}
|
|
|
|
// Space before unary minus followed by a function call.
|
|
if (
|
|
options.parser === "scss" &&
|
|
isMathOperator &&
|
|
iNode.value === "-" &&
|
|
iNextNode.type === "value-func" &&
|
|
locEnd(iNode) !== locStart(iNextNode)
|
|
) {
|
|
parts.push([parts.pop(), " "]);
|
|
continue;
|
|
}
|
|
|
|
const iNextNextNode = node.groups[i + 2];
|
|
|
|
// 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?.type === "value-func" ||
|
|
(iNextNextNode && isWordNode(iNextNextNode)) ||
|
|
iNode.type === "value-func" ||
|
|
isWordNode(iNode);
|
|
const requireSpaceAfterOperator =
|
|
iNextNode.type === "value-func" ||
|
|
isWordNode(iNextNode) ||
|
|
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;
|
|
}
|
|
|
|
// No space before unary minus followed by an opening parenthesis `-(`
|
|
if (
|
|
(options.parser === "scss" || options.parser === "less") &&
|
|
isMathOperator &&
|
|
iNode.value === "-" &&
|
|
isParenGroupNode(iNextNode) &&
|
|
locEnd(iNode) === locStart(iNextNode.open) &&
|
|
iNextNode.open.value === "("
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// Add `hardline` after inline comment (i.e. `// comment\n foo: bar;`)
|
|
if (isInlineValueCommentNode(iNode)) {
|
|
if (parentNode.type === "value-paren_group") {
|
|
parts.push(dedent(hardline), "");
|
|
continue;
|
|
}
|
|
parts.push(hardline, "");
|
|
continue;
|
|
}
|
|
|
|
// Handle keywords in SCSS control directive
|
|
if (
|
|
isControlDirective &&
|
|
(isEqualityOperatorNode(iNextNode) ||
|
|
isRelationalOperatorNode(iNextNode) ||
|
|
isIfElseKeywordNode(iNextNode) ||
|
|
isEachKeywordNode(iNode) ||
|
|
isForKeywordNode(iNode))
|
|
) {
|
|
parts.push([parts.pop(), " "]);
|
|
|
|
continue;
|
|
}
|
|
|
|
// At-rule `namespace` should be in one line
|
|
if (
|
|
atRuleAncestorNode &&
|
|
atRuleAncestorNode.name.toLowerCase() === "namespace"
|
|
) {
|
|
parts.push([parts.pop(), " "]);
|
|
|
|
continue;
|
|
}
|
|
|
|
// Formatting `grid` property
|
|
if (isGridValue) {
|
|
if (
|
|
iNode.source &&
|
|
iNextNode.source &&
|
|
iNode.source.start.line !== iNextNode.source.start.line
|
|
) {
|
|
parts.push(hardline, "");
|
|
|
|
didBreak = true;
|
|
} else {
|
|
parts.push([parts.pop(), " "]);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Formatting `font` property
|
|
if (
|
|
declAncestorProp &&
|
|
(declAncestorProp === "font" || declAncestorProp.startsWith("--"))
|
|
) {
|
|
if (
|
|
isDivisionNode(iNextNode) &&
|
|
hasEmptyRawBefore(iNextNode) &&
|
|
isPossibleFontSize(iNode)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
isDivisionNode(iNode) &&
|
|
hasEmptyRawBefore(iNode) &&
|
|
isPossibleFontSize(iPrevNode)
|
|
) {
|
|
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([parts.pop(), " "]);
|
|
|
|
continue;
|
|
}
|
|
// allow function(returns-list($list)...)
|
|
if (iNextNode?.value === "...") {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
isAtWordPlaceholderNode(iNode) &&
|
|
isAtWordPlaceholderNode(iNextNode) &&
|
|
locEnd(iNode) === locStart(iNextNode)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
isAtWordPlaceholderNode(iNode) &&
|
|
isParenGroupNode(iNextNode) &&
|
|
locEnd(iNode) === locStart(iNextNode.open)
|
|
) {
|
|
parts.push(softline, "");
|
|
continue;
|
|
}
|
|
|
|
if (iNode.value === "with" && isParenGroupNode(iNextNode)) {
|
|
parts = [[fill(parts), " "]];
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
iNode.value?.endsWith("#") &&
|
|
iNextNode.value === "{" &&
|
|
isParenGroupNode(iNextNode.group)
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// don't print line when the next node is a comment and last node
|
|
// it will be printed with the comment in a line suffix
|
|
if (isInlineValueCommentNode(iNextNode) && !iNextNextNode) {
|
|
continue;
|
|
}
|
|
|
|
// align value after block comment
|
|
if (
|
|
!atRuleAncestorNode &&
|
|
iNode.type === "value-comment" &&
|
|
!iNode.inline &&
|
|
node.groups.slice(0, i).every((group) => group.type === "value-comment")
|
|
) {
|
|
parts.push(dedent(line), "");
|
|
continue;
|
|
}
|
|
|
|
// Be default all values go through `line`
|
|
parts.push(line, "");
|
|
}
|
|
|
|
if (hasInlineComment) {
|
|
parts.push([parts.pop(), breakParent]);
|
|
}
|
|
|
|
if (didBreak) {
|
|
parts.unshift("", hardline);
|
|
}
|
|
|
|
if (isControlDirective) {
|
|
return group(indent(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)));
|
|
}
|
|
|
|
function isPossibleFontSize(node) {
|
|
if (node?.type === "value-number") {
|
|
return true;
|
|
}
|
|
|
|
if (node?.type !== "value-func") {
|
|
return false;
|
|
}
|
|
|
|
const value = node.value.toLowerCase();
|
|
return (
|
|
value === "var" ||
|
|
value === "calc" ||
|
|
value === "min" ||
|
|
value === "max" ||
|
|
value === "clamp" ||
|
|
value.startsWith("--")
|
|
);
|
|
}
|
|
|
|
export default printCommaSeparatedValueGroup;
|