From f426cfafb7f6557d0ab16b4f31726704aac4147c Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Tue, 28 Jul 2026 08:07:34 +0500 Subject: [PATCH] Keep leading `+` glued in An+B notation (#19738) --- changelog_unreleased/css/19738.md | 18 +++++++ src/language-css/printer-postcss.js | 27 ++++++++-- .../__snapshots__/format.test.js.snap | 51 +++++++++++++++++++ tests/format/css/combinator/an-plus-b.css | 19 +++++++ 4 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 changelog_unreleased/css/19738.md create mode 100644 tests/format/css/combinator/an-plus-b.css diff --git a/changelog_unreleased/css/19738.md b/changelog_unreleased/css/19738.md new file mode 100644 index 000000000..3c4add519 --- /dev/null +++ b/changelog_unreleased/css/19738.md @@ -0,0 +1,18 @@ +#### Keep leading `+` glued in An+B notation (#19738 by @lazerg) + +The An+B grammar doesn't allow whitespace between a leading `+` and the `n`, so the extra space produced invalid output. + + +```css +/* Input */ +a:nth-child(+3n+2) { +} + +/* Prettier stable */ +a:nth-child(+ 3n + 2) { +} + +/* Prettier main */ +a:nth-child(+3n + 2) { +} +``` diff --git a/src/language-css/printer-postcss.js b/src/language-css/printer-postcss.js index e04ec746a..1c30081be 100644 --- a/src/language-css/printer-postcss.js +++ b/src/language-css/printer-postcss.js @@ -350,13 +350,30 @@ function genericPrint(path, options, print) { node.value === ">>>" ) { const parentNode = path.parent; - const leading = + const isFirstNode = parentNode.type === "selector-selector" && - parentNode.nodes[0] === node - ? "" - : line; + parentNode.nodes[0] === node; + const leading = isFirstNode ? "" : line; - return [leading, node.value, path.isLast ? "" : " "]; + // The leading `+` in An+B notation (e.g. `:nth-child(+2n)`) must stay + // glued to the following token, a space there is invalid per spec. + const grandparentNode = path.grandparent; + const isNthLeadingSign = + isFirstNode && + node.value === "+" && + grandparentNode?.type === "selector-pseudo" && + [ + ":nth-child", + ":nth-last-child", + ":nth-of-type", + ":nth-last-of-type", + ].includes(grandparentNode.value.toLowerCase()); + + return [ + leading, + node.value, + path.isLast || isNthLeadingSign ? "" : " ", + ]; } const leading = node.value.trimStart().startsWith("(") ? line : ""; diff --git a/tests/format/css/combinator/__snapshots__/format.test.js.snap b/tests/format/css/combinator/__snapshots__/format.test.js.snap index ee1c44007..2071350a8 100644 --- a/tests/format/css/combinator/__snapshots__/format.test.js.snap +++ b/tests/format/css/combinator/__snapshots__/format.test.js.snap @@ -1,5 +1,56 @@ // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +exports[`an-plus-b.css format 1`] = ` +====================================options===================================== +parsers: ["css"] + printWidth: 80 (default) | +=====================================input====================================== +:nth-child(+2n), +:nth-child(+n), +:nth-child(+3n+2), +:nth-last-child(+3n+1), +:nth-of-type(+2n), +:nth-last-of-type(+n+3), +:nth-child(2n+1), +:nth-child(-n+2), +:not(:nth-child(+2n)), +:not(:nth-child(+n)), +:not(:nth-child(+3n+2)), +:not(:nth-last-child(+3n+1)), +:not(:nth-of-type(+2n)), +:not(:nth-last-of-type(+n+3)), +:not(:nth-child(2n+1)), +:not(:nth-child(-n+2)) {} + +:has(+ img), +:has(+img) {} + +=====================================output===================================== +:nth-child(+2n), +:nth-child(+n), +:nth-child(+3n + 2), +:nth-last-child(+3n + 1), +:nth-of-type(+2n), +:nth-last-of-type(+n + 3), +:nth-child(2n + 1), +:nth-child(-n + 2), +:not(:nth-child(+2n)), +:not(:nth-child(+n)), +:not(:nth-child(+3n + 2)), +:not(:nth-last-child(+3n + 1)), +:not(:nth-of-type(+2n)), +:not(:nth-last-of-type(+n + 3)), +:not(:nth-child(2n + 1)), +:not(:nth-child(-n + 2)) { +} + +:has(+ img), +:has(+ img) { +} + +================================================================================ +`; + exports[`combinator.css format 1`] = ` ====================================options===================================== parsers: ["css"] diff --git a/tests/format/css/combinator/an-plus-b.css b/tests/format/css/combinator/an-plus-b.css new file mode 100644 index 000000000..513b34a3a --- /dev/null +++ b/tests/format/css/combinator/an-plus-b.css @@ -0,0 +1,19 @@ +:nth-child(+2n), +:nth-child(+n), +:nth-child(+3n+2), +:nth-last-child(+3n+1), +:nth-of-type(+2n), +:nth-last-of-type(+n+3), +:nth-child(2n+1), +:nth-child(-n+2), +:not(:nth-child(+2n)), +:not(:nth-child(+n)), +:not(:nth-child(+3n+2)), +:not(:nth-last-child(+3n+1)), +:not(:nth-of-type(+2n)), +:not(:nth-last-of-type(+n+3)), +:not(:nth-child(2n+1)), +:not(:nth-child(-n+2)) {} + +:has(+ img), +:has(+img) {}