Files
test-repo/src/language-css/printer-postcss.js
T
fisker Cheung 0cd7e10447 Merge branch 'main' into next
# Conflicts:
#	docs/browser.md
#	src/language-js/print/literal.js
#	src/language-js/print/misc.js
#	src/language-js/printer-estree.js
#	tests/format/markdown/auto-link/jsfmt.spec.js
#	yarn.lock
2023-01-10 09:38:45 +08:00

1174 lines
34 KiB
JavaScript

import {
printNumber,
printString,
hasNewline,
isFrontMatterNode,
isNextLineEmpty,
isNonEmptyArray,
} from "../common/util.js";
import {
join,
line,
hardline,
softline,
group,
fill,
indent,
dedent,
ifBreak,
breakParent,
} from "../document/builders.js";
import { removeLines, getDocParts } from "../document/utils.js";
import UnexpectedNodeError from "../utils/unexpected-node-error.js";
import clean from "./clean.js";
import embed from "./embed.js";
import { insertPragma } from "./pragma.js";
import getVisitorKeys from "./get-visitor-keys.js";
import {
getPropOfDeclNode,
maybeToLowerCase,
insideValueFunctionNode,
insideICSSRuleNode,
insideAtRuleNode,
insideURLFunctionInImportAtRuleNode,
isKeyframeAtRuleKeywords,
isWideKeywords,
isLastNode,
isSCSSControlDirectiveNode,
isDetachedRulesetDeclarationNode,
isRelationalOperatorNode,
isEqualityOperatorNode,
isMultiplicationNode,
isDivisionNode,
isAdditionNode,
isSubtractionNode,
isMathOperatorNode,
isEachKeywordNode,
isForKeywordNode,
isURLFunctionNode,
isIfElseKeywordNode,
hasComposesNode,
hasParensAroundNode,
hasEmptyRawBefore,
isKeyValuePairNode,
isKeyInValuePairNode,
isDetachedRulesetCallNode,
isTemplatePlaceholderNode,
isTemplatePropNode,
isPostcssSimpleVarNode,
isSCSSMapItemNode,
isInlineValueCommentNode,
isHashNode,
isLeftCurlyBraceNode,
isRightCurlyBraceNode,
isWordNode,
isColonNode,
isMediaAndSupportsKeywords,
isColorAdjusterFuncNode,
lastLineHasInlineComment,
isAtWordPlaceholderNode,
isConfigurationNode,
isParenGroupNode,
isVarFunctionNode,
} from "./utils/index.js";
import { locStart, locEnd } from "./loc.js";
import printUnit from "./utils/print-unit.js";
function shouldPrintComma(options) {
return options.trailingComma === "es5" || options.trailingComma === "all";
}
function genericPrint(path, options, print) {
const { node } = path;
switch (node.type) {
case "front-matter":
return [node.raw, hardline];
case "css-root": {
const nodes = printNodeSequence(path, options, print);
let after = node.raws.after.trim();
if (after.startsWith(";")) {
after = after.slice(1).trim();
}
return [
node.frontMatter ? [print("frontMatter"), hardline] : "",
nodes,
after ? ` ${after}` : "",
node.nodes.length > 0 ? hardline : "",
];
}
case "css-comment": {
const isInlineComment = node.inline || node.raws.inline;
const text = options.originalText.slice(locStart(node), locEnd(node));
return isInlineComment ? text.trimEnd() : text;
}
case "css-rule":
return [
print("selector"),
node.important ? " !important" : "",
node.nodes
? [
node.selector?.type === "selector-unknown" &&
lastLineHasInlineComment(node.selector.value)
? line
: node.selector
? " "
: "",
"{",
node.nodes.length > 0
? indent([hardline, printNodeSequence(path, options, print)])
: "",
hardline,
"}",
isDetachedRulesetDeclarationNode(node) ? ";" : "",
]
: ";",
];
case "css-decl": {
const parentNode = path.parent;
const { between: rawBetween } = node.raws;
const trimmedBetween = rawBetween.trim();
const isColon = trimmedBetween === ":";
const isValueAllSpace =
typeof node.value === "string" && /^ *$/.test(node.value);
let value = typeof node.value === "string" ? node.value : print("value");
value = hasComposesNode(node) ? removeLines(value) : value;
if (!isColon && lastLineHasInlineComment(trimmedBetween)) {
value = indent([hardline, dedent(value)]);
}
return [
node.raws.before.replaceAll(/[\s;]/g, ""),
// Less variable
(parentNode.type === "css-atrule" && parentNode.variable) ||
insideICSSRuleNode(path)
? node.prop
: maybeToLowerCase(node.prop),
trimmedBetween.startsWith("//") ? " " : "",
trimmedBetween,
node.extend || isValueAllSpace ? "" : " ",
options.parser === "less" && node.extend && node.selector
? ["extend(", print("selector"), ")"]
: "",
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
? [
" {",
indent([softline, printNodeSequence(path, options, print)]),
softline,
"}",
]
: isTemplatePropNode(node) &&
!parentNode.raws.semicolon &&
options.originalText[locEnd(node) - 1] !== ";"
? ""
: options.__isHTMLStyleAttribute && isLastNode(path, node)
? ifBreak(";")
: ";",
];
}
case "css-atrule": {
const parentNode = path.parent;
const isTemplatePlaceholderNodeWithoutSemiColon =
isTemplatePlaceholderNode(node) &&
!parentNode.raws.semicolon &&
options.originalText[locEnd(node) - 1] !== ";";
if (options.parser === "less") {
if (node.mixin) {
return [
print("selector"),
node.important ? " !important" : "",
isTemplatePlaceholderNodeWithoutSemiColon ? "" : ";",
];
}
if (node.function) {
return [
node.name,
typeof node.params === "string" ? node.params : print("params"),
isTemplatePlaceholderNodeWithoutSemiColon ? "" : ";",
];
}
if (node.variable) {
return [
"@",
node.name,
": ",
node.value ? print("value") : "",
node.raws.between.trim() ? node.raws.between.trim() + " " : "",
node.nodes
? [
"{",
indent([
node.nodes.length > 0 ? softline : "",
printNodeSequence(path, options, print),
]),
softline,
"}",
]
: "",
isTemplatePlaceholderNodeWithoutSemiColon ? "" : ";",
];
}
}
const isImportUnknownValueEndsWithSemiColon =
node.name === "import" &&
node.params?.type === "value-unknown" &&
node.params.value.endsWith(";");
return [
"@",
// 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
? [
isDetachedRulesetCallNode(node)
? ""
: isTemplatePlaceholderNode(node)
? node.raws.afterName === ""
? ""
: node.name.endsWith(":")
? " "
: /^\s*\n\s*\n/.test(node.raws.afterName)
? [hardline, hardline]
: /^\s*\n/.test(node.raws.afterName)
? hardline
: " "
: " ",
typeof node.params === "string" ? node.params : print("params"),
]
: "",
node.selector ? indent([" ", print("selector")]) : "",
node.value
? group([
" ",
print("value"),
isSCSSControlDirectiveNode(node, options)
? hasParensAroundNode(node)
? " "
: line
: "",
])
: node.name === "else"
? " "
: "",
node.nodes
? [
isSCSSControlDirectiveNode(node, options)
? ""
: (node.selector &&
!node.selector.nodes &&
typeof node.selector.value === "string" &&
lastLineHasInlineComment(node.selector.value)) ||
(!node.selector &&
typeof node.params === "string" &&
lastLineHasInlineComment(node.params))
? line
: " ",
"{",
indent([
node.nodes.length > 0 ? softline : "",
printNodeSequence(path, options, print),
]),
softline,
"}",
]
: isTemplatePlaceholderNodeWithoutSemiColon ||
isImportUnknownValueEndsWithSemiColon
? ""
: ";",
];
}
// postcss-media-query-parser
case "media-query-list": {
const parts = [];
path.each(({ node }) => {
if (node.type === "media-query" && node.value === "") {
return;
}
parts.push(print());
}, "nodes");
return group(indent(join(line, parts)));
}
case "media-query":
return [
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 ["(", ...path.map(print, "nodes"), ")"];
case "media-feature":
return maybeToLowerCase(
adjustStrings(node.value.replaceAll(/ +/g, " "), options)
);
case "media-colon":
return [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.replaceAll(/^url\(\s+/gi, "url(").replaceAll(/\s+\)$/g, ")"),
options
);
case "media-unknown":
return node.value;
// postcss-selector-parser
case "selector-root":
return group([
insideAtRuleNode(path, "custom-selector")
? [
path.findAncestor((node) => node.type === "css-atrule")
.customSelector,
line,
]
: "",
join(
[
",",
insideAtRuleNode(path, ["extend", "custom-selector", "nest"])
? line
: hardline,
],
path.map(print, "nodes")
),
]);
case "selector-selector":
return group(indent(path.map(print, "nodes")));
case "selector-comment":
return node.value;
case "selector-string":
return adjustStrings(node.value, options);
case "selector-tag":
return [
node.namespace
? [node.namespace === true ? "" : node.namespace.trim(), "|"]
: "",
path.previous?.type === "selector-nesting"
? node.value
: adjustNumbers(
isKeyframeAtRuleKeywords(path, node.value)
? node.value.toLowerCase()
: node.value
),
];
case "selector-id":
return ["#", node.value];
case "selector-class":
return [".", adjustNumbers(adjustStrings(node.value, options))];
case "selector-attribute":
return [
"[",
node.namespace
? [node.namespace === true ? "" : node.namespace.trim(), "|"]
: "",
node.attribute.trim(),
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.parent;
const leading =
parentNode.type === "selector-selector" &&
parentNode.nodes[0] === node
? ""
: line;
return [leading, node.value, isLastNode(path, node) ? "" : " "];
}
const leading = node.value.trim().startsWith("(") ? line : "";
const value =
adjustNumbers(adjustStrings(node.value.trim(), options)) || line;
return [leading, value];
}
case "selector-universal":
return [
node.namespace
? [node.namespace === true ? "" : node.namespace.trim(), "|"]
: "",
node.value,
];
case "selector-pseudo":
return [
maybeToLowerCase(node.value),
isNonEmptyArray(node.nodes)
? group([
"(",
indent([softline, join([",", line], path.map(print, "nodes"))]),
softline,
")",
])
: "",
];
case "selector-nesting":
return node.value;
case "selector-unknown": {
const ruleAncestorNode = path.findAncestor(
(node) => node.type === "css-rule"
);
// Nested SCSS property
if (ruleAncestorNode?.isSCSSNesterProperty) {
return adjustNumbers(
adjustStrings(maybeToLowerCase(node.value), options)
);
}
// originalText has to be used for Less, see replaceQuotesInInlineComments in loc.js
const parentNode = path.parent;
if (parentNode.raws?.selector) {
const start = locStart(parentNode);
const end = start + parentNode.raws.selector.length;
return options.originalText.slice(start, end).trim();
}
// Same reason above
const grandParent = path.grandparent;
if (
parentNode.type === "value-paren_group" &&
grandParent?.type === "value-func" &&
grandParent.value === "selector"
) {
const start = locEnd(parentNode.open) + 1;
const end = locStart(parentNode.close);
const selector = options.originalText.slice(start, end).trim();
return lastLineHasInlineComment(selector)
? [breakParent, selector]
: selector;
}
return node.value;
}
// postcss-values-parser
case "value-value":
case "value-root":
return print("group");
case "value-comment":
return options.originalText.slice(locStart(node), locEnd(node));
case "value-comma_group": {
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");
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]);
const iPrevNode = node.groups[i - 1];
const iNode = node.groups[i];
const iNextNode = node.groups[i + 1];
const iNextNextNode = node.groups[i + 2];
if (insideURLFunction) {
if (
(iNextNode && isAdditionNode(iNextNode)) ||
isAdditionNode(iNode)
) {
parts.push(" ");
}
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 after latest node (i.e. before semicolon)
if (!iNextNode) {
continue;
}
// styled.div` background: var(--${one}); `
if (
iNode.type === "value-word" &&
iNode.value.endsWith("-") &&
isAtWordPlaceholderNode(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;
}
// 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;
}
// 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(" ");
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 &&
iNextNode.source &&
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;
}
// 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.push(" ");
continue;
}
if (
iNode.value?.endsWith("#") &&
iNextNode.value === "{" &&
isParenGroupNode(iNextNode.group)
) {
continue;
}
// Be default all values go through `line`
parts.push(line);
}
if (hasInlineComment) {
parts.push(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)));
}
case "value-paren_group": {
const parentNode = path.parent;
const printedGroups = path.map(() => {
const child = path.node;
return typeof child === "string" ? child : print();
}, "groups");
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 [
node.open ? print("open") : "",
join(",", printedGroups),
node.close ? print("close") : "",
];
}
if (!node.open) {
return group(indent(fill(join([",", line], printedGroups))));
}
const isSCSSMapItem = isSCSSMapItemNode(path, options);
const lastItem = node.groups.at(-1);
const isLastItemComment = lastItem?.type === "value-comment";
const isKey = isKeyInValuePairNode(node, parentNode);
const isConfiguration = isConfigurationNode(node, parentNode);
const isVarFunction = isVarFunctionNode(parentNode);
const shouldBreak = isConfiguration || (isSCSSMapItem && !isKey);
const shouldDedent = isConfiguration || isKey;
const printed = group(
[
node.open ? print("open") : "",
indent([
softline,
join(
[line],
path.map(({ node: child, isLast, index }) => {
const hasComma = () =>
Boolean(
child.source &&
options.originalText
.slice(locStart(child), locStart(node.close))
.trimEnd()
.endsWith(",")
);
const shouldPrintComma =
!isLast || (isVarFunction && hasComma());
let printed = [
printedGroups[index],
shouldPrintComma ? "," : "",
];
// Key/Value pair in open paren already indented
if (
isKeyValuePairNode(child) &&
child.type === "value-comma_group" &&
child.groups &&
child.groups[0].type !== "value-paren_group" &&
child.groups[2]?.type === "value-paren_group"
) {
const parts = getDocParts(printed[0].contents.contents);
parts[1] = group(parts[1]);
printed = [group(dedent(printed))];
}
if (
!isLast &&
child.type === "value-comma_group" &&
isNonEmptyArray(child.groups)
) {
let last = child.groups.at(-1);
// `value-paren_group` does not have location info, but its closing parenthesis does.
if (!last.source && last.close) {
last = last.close;
}
if (
last.source &&
isNextLineEmpty(options.originalText, last, locEnd)
) {
printed.push(hardline);
}
}
return printed;
}, "groups")
),
]),
ifBreak(
!isLastItemComment &&
options.parser === "scss" &&
isSCSSMapItem &&
shouldPrintComma(options)
? ","
: ""
),
softline,
node.close ? print("close") : "",
],
{
shouldBreak,
}
);
return shouldDedent ? dedent(printed) : printed;
}
case "value-func":
return [
node.value,
insideAtRuleNode(path, "supports") && isMediaAndSupportsKeywords(node)
? " "
: "",
print("group"),
];
case "value-paren":
return node.value;
case "value-number":
return [printCssNumber(node.value), printUnit(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": {
const { previous } = path;
return [
node.value,
// Don't add spaces on escaped colon `:`, e.g: grid-template-rows: [row-1-00\:00] auto;
(typeof previous?.value === "string" &&
previous.value.endsWith("\\")) ||
// Don't add spaces on `:` in `url` function (i.e. `url(fbglyph: cross-outline, fig-white)`)
insideValueFunctionNode(path, "url")
? ""
: line,
];
}
case "value-string":
return printString(
node.raws.quote + node.value + node.raws.quote,
options
);
case "value-atword":
return ["@", node.value];
case "value-unicode-range":
return node.value;
case "value-unknown":
return node.value;
case "value-comma": // Handled in `value-comma_group`
default:
/* c8 ignore next */
throw new UnexpectedNodeError(node, "PostCSS");
}
}
function printNodeSequence(path, options, print) {
const parts = [];
path.each(() => {
const { node, previous } = path;
if (
previous?.type === "css-comment" &&
previous.text.trim() === "prettier-ignore"
) {
parts.push(options.originalText.slice(locStart(node), locEnd(node)));
} else {
parts.push(print());
}
if (path.isLast) {
return;
}
const { next } = path;
if (
(next.type === "css-comment" &&
!hasNewline(options.originalText, locStart(next), {
backwards: true,
}) &&
!isFrontMatterNode(node)) ||
(next.type === "css-atrule" &&
next.name === "else" &&
node.type !== "css-comment")
) {
parts.push(" ");
} else {
parts.push(options.__isHTMLStyleAttribute ? line : hardline);
if (
isNextLineEmpty(options.originalText, node, locEnd) &&
!isFrontMatterNode(node)
) {
parts.push(hardline);
}
}
}, "nodes");
return parts;
}
const STRING_REGEX = /(["'])(?:(?!\1)[^\\]|\\.)*\1/gs;
const NUMBER_REGEX = /(?:\d*\.\d+|\d+\.?)(?:[Ee][+-]?\d+)?/g;
const STANDARD_UNIT_REGEX = /[A-Za-z]+/g;
const WORD_PART_REGEX = /[$@]?[A-Z_a-z\u0080-\uFFFF][\w\u0080-\uFFFF-]*/g;
const ADJUST_NUMBERS_REGEX = new RegExp(
STRING_REGEX.source +
"|" +
`(${WORD_PART_REGEX.source})?` +
`(${NUMBER_REGEX.source})` +
`(${STANDARD_UNIT_REGEX.source})?`,
"g"
);
function adjustStrings(value, options) {
return value.replaceAll(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
? printCssNumber(number) + maybeToLowerCase(unit || "")
: match
);
}
function printCssNumber(rawNumber) {
return (
printNumber(rawNumber)
// Remove trailing `.0`.
.replace(/\.0(?=$|e)/, "")
);
}
const printer = {
print: genericPrint,
embed,
insertPragma,
massageAstNode: clean,
getVisitorKeys,
};
export default printer;