From 346a571b2a09adc777cc8cab9bed0a764ecd5739 Mon Sep 17 00:00:00 2001
From: Romain Menke
Date: Sun, 21 Jan 2024 19:03:35 +0100
Subject: [PATCH 1/2] selector-resolve-nested
---
package-lock.json | 28 ++
packages/selector-resolve-nested/.gitignore | 6 +
packages/selector-resolve-nested/.nvmrc | 1 +
packages/selector-resolve-nested/CHANGELOG.md | 5 +
packages/selector-resolve-nested/LICENSE.md | 18 ++
packages/selector-resolve-nested/README.md | 33 +++
.../api-extractor.json | 7 +
.../selector-resolve-nested/dist/index.cjs | 1 +
.../selector-resolve-nested/dist/index.d.ts | 33 +++
.../selector-resolve-nested/dist/index.mjs | 1 +
.../selector-resolve-nested/docs/index.md | 12 +
.../docs/selector-resolve-nested.api.json | 241 ++++++++++++++++++
.../docs/selector-resolve-nested.md | 30 +++
...or-resolve-nested.resolvenestedselector.md | 27 ++
packages/selector-resolve-nested/package.json | 79 ++++++
.../src/compound-selector-order.ts | 90 +++++++
packages/selector-resolve-nested/src/index.ts | 118 +++++++++
.../selector-resolve-nested/stryker.conf.json | 19 ++
.../selector-resolve-nested/test/_import.mjs | 8 +
.../selector-resolve-nested/test/_require.cjs | 8 +
.../selector-resolve-nested/test/complex.mjs | 101 ++++++++
.../selector-resolve-nested/test/compound.mjs | 146 +++++++++++
.../selector-resolve-nested/test/index.mjs | 5 +
.../selector-resolve-nested/test/lists.mjs | 49 ++++
.../test/multiple-resolves.mjs | 21 ++
.../test/simple-compound-complex.mjs | 77 ++++++
.../test/util/parse.mjs | 6 +
.../selector-resolve-nested/tsconfig.json | 10 +
28 files changed, 1180 insertions(+)
create mode 100644 packages/selector-resolve-nested/.gitignore
create mode 100644 packages/selector-resolve-nested/.nvmrc
create mode 100644 packages/selector-resolve-nested/CHANGELOG.md
create mode 100644 packages/selector-resolve-nested/LICENSE.md
create mode 100644 packages/selector-resolve-nested/README.md
create mode 100644 packages/selector-resolve-nested/api-extractor.json
create mode 100644 packages/selector-resolve-nested/dist/index.cjs
create mode 100644 packages/selector-resolve-nested/dist/index.d.ts
create mode 100644 packages/selector-resolve-nested/dist/index.mjs
create mode 100644 packages/selector-resolve-nested/docs/index.md
create mode 100644 packages/selector-resolve-nested/docs/selector-resolve-nested.api.json
create mode 100644 packages/selector-resolve-nested/docs/selector-resolve-nested.md
create mode 100644 packages/selector-resolve-nested/docs/selector-resolve-nested.resolvenestedselector.md
create mode 100644 packages/selector-resolve-nested/package.json
create mode 100644 packages/selector-resolve-nested/src/compound-selector-order.ts
create mode 100644 packages/selector-resolve-nested/src/index.ts
create mode 100644 packages/selector-resolve-nested/stryker.conf.json
create mode 100644 packages/selector-resolve-nested/test/_import.mjs
create mode 100644 packages/selector-resolve-nested/test/_require.cjs
create mode 100644 packages/selector-resolve-nested/test/complex.mjs
create mode 100644 packages/selector-resolve-nested/test/compound.mjs
create mode 100644 packages/selector-resolve-nested/test/index.mjs
create mode 100644 packages/selector-resolve-nested/test/lists.mjs
create mode 100644 packages/selector-resolve-nested/test/multiple-resolves.mjs
create mode 100644 packages/selector-resolve-nested/test/simple-compound-complex.mjs
create mode 100644 packages/selector-resolve-nested/test/util/parse.mjs
create mode 100644 packages/selector-resolve-nested/tsconfig.json
diff --git a/package-lock.json b/package-lock.json
index 31da73f77..cc65443cb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2189,6 +2189,10 @@
"resolved": "plugins/postcss-unset-value",
"link": true
},
+ "node_modules/@csstools/selector-resolve-nested": {
+ "resolved": "packages/selector-resolve-nested",
+ "link": true
+ },
"node_modules/@csstools/selector-specificity": {
"resolved": "packages/selector-specificity",
"link": true
@@ -11095,6 +11099,30 @@
"node": "^14 || ^16 || >=18"
}
},
+ "packages/selector-resolve-nested": {
+ "name": "@csstools/selector-resolve-nested",
+ "version": "0.0.0",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "devDependencies": {
+ "postcss-selector-parser": "^6.0.13"
+ },
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "peerDependencies": {
+ "postcss-selector-parser": "^6.0.13"
+ }
+ },
"packages/selector-specificity": {
"name": "@csstools/selector-specificity",
"version": "3.0.1",
diff --git a/packages/selector-resolve-nested/.gitignore b/packages/selector-resolve-nested/.gitignore
new file mode 100644
index 000000000..e5b28db4a
--- /dev/null
+++ b/packages/selector-resolve-nested/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+package-lock.json
+yarn.lock
+*.result.css
+*.result.css.map
+*.result.html
diff --git a/packages/selector-resolve-nested/.nvmrc b/packages/selector-resolve-nested/.nvmrc
new file mode 100644
index 000000000..6ed5da955
--- /dev/null
+++ b/packages/selector-resolve-nested/.nvmrc
@@ -0,0 +1 @@
+v20.2.0
diff --git a/packages/selector-resolve-nested/CHANGELOG.md b/packages/selector-resolve-nested/CHANGELOG.md
new file mode 100644
index 000000000..683037fd1
--- /dev/null
+++ b/packages/selector-resolve-nested/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changes to Selector Resolve Nested
+
+### Unreleased (major)
+
+- Initial version
diff --git a/packages/selector-resolve-nested/LICENSE.md b/packages/selector-resolve-nested/LICENSE.md
new file mode 100644
index 000000000..e8ae93b9f
--- /dev/null
+++ b/packages/selector-resolve-nested/LICENSE.md
@@ -0,0 +1,18 @@
+MIT No Attribution (MIT-0)
+
+Copyright © CSSTools Contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the “Software”), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/selector-resolve-nested/README.md b/packages/selector-resolve-nested/README.md
new file mode 100644
index 000000000..9f7aa7b00
--- /dev/null
+++ b/packages/selector-resolve-nested/README.md
@@ -0,0 +1,33 @@
+# Selector Resolve Nested
+
+[
][npm-url]
+[
][cli-url]
+[
][discord]
+
+## API
+
+[Read the API docs](./docs/selector-resolve-nested.md)
+
+## Usage
+
+Add [Selector Resolve Nested] to your project:
+
+```bash
+npm install @csstools/selector-resolve-nested --save-dev
+```
+
+```js
+import { resolveNestedSelector } from '@csstools/selector-resolve-nested';
+import parser from 'postcss-selector-parser';
+
+const a = parser().astSync('.foo &');
+const b = parser().astSync('.bar');
+
+resolveNestedSelector(a, b); // '.foo .bar'
+```
+
+[cli-url]: https://github.com/csstools/postcss-plugins/actions/workflows/test.yml?query=workflow/test
+[discord]: https://discord.gg/bUadyRwkJS
+[npm-url]: https://www.npmjs.com/package/@csstools/selector-resolve-nested
+
+[Selector Resolve Nested]: https://github.com/csstools/postcss-plugins/tree/main/packages/selector-resolve-nested
diff --git a/packages/selector-resolve-nested/api-extractor.json b/packages/selector-resolve-nested/api-extractor.json
new file mode 100644
index 000000000..ccef9beeb
--- /dev/null
+++ b/packages/selector-resolve-nested/api-extractor.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
+ "extends": "../../api-extractor.json",
+ "docModel": {
+ "enabled": true
+ }
+}
diff --git a/packages/selector-resolve-nested/dist/index.cjs b/packages/selector-resolve-nested/dist/index.cjs
new file mode 100644
index 000000000..75b873f51
--- /dev/null
+++ b/packages/selector-resolve-nested/dist/index.cjs
@@ -0,0 +1 @@
+"use strict";var e=require("postcss-selector-parser");function sortCompoundSelectorsInsideComplexSelector(t){const o=[];let r=[];t.each((t=>{if("combinator"===t.type)return o.push(r,[t]),void(r=[]);if(e.isPseudoElement(t))return o.push(r),void(r=[t]);if("universal"===t.type&&r.find((e=>"universal"===e.type)))t.remove();else{if("tag"===t.type&&r.find((e=>"tag"===e.type))){t.remove();const o=e.pseudo({value:":is"});return o.append(e.selector({nodes:[t],value:""})),void r.push(o)}r.push(t)}})),o.push(r);const n=[];for(let e=0;eselectorTypeOrder(e)-selectorTypeOrder(t))),n.push(...t)}t.removeAll();for(let e=n.length-1;e>=0;e--)n[e].remove(),t.prepend(n[e])}function selectorTypeOrder(o){return e.isPseudoElement(o)?t.pseudoElement:t[o.type]}const t={universal:0,tag:1,pseudoElement:2,nesting:3,id:4,class:5,attribute:6,pseudo:7,comment:8};function prepareParentSelectors(t,o=!1){return o||!isCompoundSelector(t.nodes)?[e.pseudo({value:":is",nodes:t.nodes.map((e=>e.clone()))})]:t.nodes[0].nodes.map((e=>e.clone()))}function isCompoundSelector(t){return 1===t.length&&!t[0].nodes.some((t=>"combinator"===t.type||e.isPseudoElement(t)))}exports.resolveNestedSelector=function resolveNestedSelector(t,o){const r=[];for(let n=0;n(t=!0,!1))),t?"combinator"===s.nodes[0]?.type&&s.prepend(e.nesting({})):(s.prepend(e.combinator({value:" "})),s.prepend(e.nesting({})))}{const e=new Set;s.walkNesting((t=>{const r=t.parent;e.add(r),"pseudo"===r.parent?.type&&":has"===r.parent.value?.toLowerCase()?t.replaceWith(...prepareParentSelectors(o,!0)):t.replaceWith(...prepareParentSelectors(o))}));for(const t of e)sortCompoundSelectorsInsideComplexSelector(t)}s.walk((e=>{"combinator"===e.type&&""!==e.value.trim()?(e.rawSpaceAfter=" ",e.rawSpaceBefore=" "):(e.rawSpaceAfter="",e.rawSpaceBefore="")})),r.push(s)}return e.root({nodes:r,value:""})};
diff --git a/packages/selector-resolve-nested/dist/index.d.ts b/packages/selector-resolve-nested/dist/index.d.ts
new file mode 100644
index 000000000..0e34cf662
--- /dev/null
+++ b/packages/selector-resolve-nested/dist/index.d.ts
@@ -0,0 +1,33 @@
+/**
+ * Resolve nested selectors following the CSS nesting specification.
+ *
+ * @example
+ *
+ * ```js
+ * import { resolveNestedSelector } from '@csstools/selector-resolve-nested';
+ * import parser from 'postcss-selector-parser';
+ *
+ * const selector = parser().astSync('.foo &');
+ * const parent = parser().astSync('.bar');
+ *
+ * // .foo .bar
+ * console.log(
+ * resolveNestedSelector(selector, parent).toString()
+ * )
+ * ```
+ *
+ * @packageDocumentation
+ */
+
+import type { Root } from 'postcss-selector-parser';
+
+/**
+ * Resolve a nested selector against a given parent selector.
+ *
+ * @param selector - The selector to resolve.
+ * @param parentSelector - The parent selector to resolve against.
+ * @returns The resolved selector.
+ */
+export declare function resolveNestedSelector(selector: Root, parentSelector: Root): Root;
+
+export { }
diff --git a/packages/selector-resolve-nested/dist/index.mjs b/packages/selector-resolve-nested/dist/index.mjs
new file mode 100644
index 000000000..1dce37960
--- /dev/null
+++ b/packages/selector-resolve-nested/dist/index.mjs
@@ -0,0 +1 @@
+import e from"postcss-selector-parser";function sortCompoundSelectorsInsideComplexSelector(o){const t=[];let r=[];o.each((o=>{if("combinator"===o.type)return t.push(r,[o]),void(r=[]);if(e.isPseudoElement(o))return t.push(r),void(r=[o]);if("universal"===o.type&&r.find((e=>"universal"===e.type)))o.remove();else{if("tag"===o.type&&r.find((e=>"tag"===e.type))){o.remove();const t=e.pseudo({value:":is"});return t.append(e.selector({nodes:[o],value:""})),void r.push(t)}r.push(o)}})),t.push(r);const n=[];for(let e=0;eselectorTypeOrder(e)-selectorTypeOrder(o))),n.push(...o)}o.removeAll();for(let e=n.length-1;e>=0;e--)n[e].remove(),o.prepend(n[e])}function selectorTypeOrder(t){return e.isPseudoElement(t)?o.pseudoElement:o[t.type]}const o={universal:0,tag:1,pseudoElement:2,nesting:3,id:4,class:5,attribute:6,pseudo:7,comment:8};function resolveNestedSelector(o,t){const r=[];for(let n=0;n(o=!0,!1))),o?"combinator"===s.nodes[0]?.type&&s.prepend(e.nesting({})):(s.prepend(e.combinator({value:" "})),s.prepend(e.nesting({})))}{const e=new Set;s.walkNesting((o=>{const r=o.parent;e.add(r),"pseudo"===r.parent?.type&&":has"===r.parent.value?.toLowerCase()?o.replaceWith(...prepareParentSelectors(t,!0)):o.replaceWith(...prepareParentSelectors(t))}));for(const o of e)sortCompoundSelectorsInsideComplexSelector(o)}s.walk((e=>{"combinator"===e.type&&""!==e.value.trim()?(e.rawSpaceAfter=" ",e.rawSpaceBefore=" "):(e.rawSpaceAfter="",e.rawSpaceBefore="")})),r.push(s)}return e.root({nodes:r,value:""})}function prepareParentSelectors(o,t=!1){return t||!isCompoundSelector(o.nodes)?[e.pseudo({value:":is",nodes:o.nodes.map((e=>e.clone()))})]:o.nodes[0].nodes.map((e=>e.clone()))}function isCompoundSelector(o){return 1===o.length&&!o[0].nodes.some((o=>"combinator"===o.type||e.isPseudoElement(o)))}export{resolveNestedSelector};
diff --git a/packages/selector-resolve-nested/docs/index.md b/packages/selector-resolve-nested/docs/index.md
new file mode 100644
index 000000000..f14b7971a
--- /dev/null
+++ b/packages/selector-resolve-nested/docs/index.md
@@ -0,0 +1,12 @@
+
+
+[Home](./index.md)
+
+## API Reference
+
+## Packages
+
+| Package | Description |
+| --- | --- |
+| [@csstools/selector-resolve-nested](./selector-resolve-nested.md) | Resolve nested selectors following the CSS nesting specification. |
+
diff --git a/packages/selector-resolve-nested/docs/selector-resolve-nested.api.json b/packages/selector-resolve-nested/docs/selector-resolve-nested.api.json
new file mode 100644
index 000000000..b00fafa65
--- /dev/null
+++ b/packages/selector-resolve-nested/docs/selector-resolve-nested.api.json
@@ -0,0 +1,241 @@
+{
+ "metadata": {
+ "toolPackage": "@microsoft/api-extractor",
+ "schemaVersion": 1011,
+ "oldestForwardsCompatibleVersion": 1001,
+ "tsdocConfig": {
+ "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
+ "noStandardTags": true,
+ "tagDefinitions": [
+ {
+ "tagName": "@alpha",
+ "syntaxKind": "modifier"
+ },
+ {
+ "tagName": "@beta",
+ "syntaxKind": "modifier"
+ },
+ {
+ "tagName": "@defaultValue",
+ "syntaxKind": "block"
+ },
+ {
+ "tagName": "@decorator",
+ "syntaxKind": "block",
+ "allowMultiple": true
+ },
+ {
+ "tagName": "@deprecated",
+ "syntaxKind": "block"
+ },
+ {
+ "tagName": "@eventProperty",
+ "syntaxKind": "modifier"
+ },
+ {
+ "tagName": "@example",
+ "syntaxKind": "block",
+ "allowMultiple": true
+ },
+ {
+ "tagName": "@experimental",
+ "syntaxKind": "modifier"
+ },
+ {
+ "tagName": "@inheritDoc",
+ "syntaxKind": "inline"
+ },
+ {
+ "tagName": "@internal",
+ "syntaxKind": "modifier"
+ },
+ {
+ "tagName": "@label",
+ "syntaxKind": "inline"
+ },
+ {
+ "tagName": "@link",
+ "syntaxKind": "inline",
+ "allowMultiple": true
+ },
+ {
+ "tagName": "@override",
+ "syntaxKind": "modifier"
+ },
+ {
+ "tagName": "@packageDocumentation",
+ "syntaxKind": "modifier"
+ },
+ {
+ "tagName": "@param",
+ "syntaxKind": "block",
+ "allowMultiple": true
+ },
+ {
+ "tagName": "@privateRemarks",
+ "syntaxKind": "block"
+ },
+ {
+ "tagName": "@public",
+ "syntaxKind": "modifier"
+ },
+ {
+ "tagName": "@readonly",
+ "syntaxKind": "modifier"
+ },
+ {
+ "tagName": "@remarks",
+ "syntaxKind": "block"
+ },
+ {
+ "tagName": "@returns",
+ "syntaxKind": "block"
+ },
+ {
+ "tagName": "@sealed",
+ "syntaxKind": "modifier"
+ },
+ {
+ "tagName": "@see",
+ "syntaxKind": "block"
+ },
+ {
+ "tagName": "@throws",
+ "syntaxKind": "block",
+ "allowMultiple": true
+ },
+ {
+ "tagName": "@typeParam",
+ "syntaxKind": "block",
+ "allowMultiple": true
+ },
+ {
+ "tagName": "@virtual",
+ "syntaxKind": "modifier"
+ },
+ {
+ "tagName": "@betaDocumentation",
+ "syntaxKind": "modifier"
+ },
+ {
+ "tagName": "@internalRemarks",
+ "syntaxKind": "block"
+ },
+ {
+ "tagName": "@preapproved",
+ "syntaxKind": "modifier"
+ }
+ ],
+ "supportForTags": {
+ "@alpha": true,
+ "@beta": true,
+ "@defaultValue": true,
+ "@decorator": true,
+ "@deprecated": true,
+ "@eventProperty": true,
+ "@example": true,
+ "@experimental": true,
+ "@inheritDoc": true,
+ "@internal": true,
+ "@label": true,
+ "@link": true,
+ "@override": true,
+ "@packageDocumentation": true,
+ "@param": true,
+ "@privateRemarks": true,
+ "@public": true,
+ "@readonly": true,
+ "@remarks": true,
+ "@returns": true,
+ "@sealed": true,
+ "@see": true,
+ "@throws": true,
+ "@typeParam": true,
+ "@virtual": true,
+ "@betaDocumentation": true,
+ "@internalRemarks": true,
+ "@preapproved": true
+ },
+ "reportUnsupportedHtmlElements": false
+ }
+ },
+ "kind": "Package",
+ "canonicalReference": "@csstools/selector-resolve-nested!",
+ "docComment": "/**\n * Resolve nested selectors following the CSS nesting specification.\n *\n * @example\n * ```js\n * import { resolveNestedSelector } from '@csstools/selector-resolve-nested';\n * import parser from 'postcss-selector-parser';\n *\n * const selector = parser().astSync('.foo &');\n * const parent = parser().astSync('.bar');\n *\n * // .foo .bar\n * console.log(\n * resolveNestedSelector(selector, parent).toString()\n * )\n * ```\n *\n * @packageDocumentation\n */\n",
+ "name": "@csstools/selector-resolve-nested",
+ "preserveMemberOrder": false,
+ "members": [
+ {
+ "kind": "EntryPoint",
+ "canonicalReference": "@csstools/selector-resolve-nested!",
+ "name": "",
+ "preserveMemberOrder": false,
+ "members": [
+ {
+ "kind": "Function",
+ "canonicalReference": "@csstools/selector-resolve-nested!resolveNestedSelector:function(1)",
+ "docComment": "/**\n * Resolve a nested selector against a given parent selector.\n *\n * @param selector - The selector to resolve.\n *\n * @param parentSelector - The parent selector to resolve against.\n *\n * @returns The resolved selector.\n */\n",
+ "excerptTokens": [
+ {
+ "kind": "Content",
+ "text": "export declare function resolveNestedSelector(selector: "
+ },
+ {
+ "kind": "Reference",
+ "text": "Root",
+ "canonicalReference": "postcss-selector-parser!parser.Root:interface"
+ },
+ {
+ "kind": "Content",
+ "text": ", parentSelector: "
+ },
+ {
+ "kind": "Reference",
+ "text": "Root",
+ "canonicalReference": "postcss-selector-parser!parser.Root:interface"
+ },
+ {
+ "kind": "Content",
+ "text": "): "
+ },
+ {
+ "kind": "Reference",
+ "text": "Root",
+ "canonicalReference": "postcss-selector-parser!parser.Root:interface"
+ },
+ {
+ "kind": "Content",
+ "text": ";"
+ }
+ ],
+ "fileUrlPath": "dist/_types/index.d.ts",
+ "returnTypeTokenRange": {
+ "startIndex": 5,
+ "endIndex": 6
+ },
+ "releaseTag": "Public",
+ "overloadIndex": 1,
+ "parameters": [
+ {
+ "parameterName": "selector",
+ "parameterTypeTokenRange": {
+ "startIndex": 1,
+ "endIndex": 2
+ },
+ "isOptional": false
+ },
+ {
+ "parameterName": "parentSelector",
+ "parameterTypeTokenRange": {
+ "startIndex": 3,
+ "endIndex": 4
+ },
+ "isOptional": false
+ }
+ ],
+ "name": "resolveNestedSelector"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/packages/selector-resolve-nested/docs/selector-resolve-nested.md b/packages/selector-resolve-nested/docs/selector-resolve-nested.md
new file mode 100644
index 000000000..a57e9c0a2
--- /dev/null
+++ b/packages/selector-resolve-nested/docs/selector-resolve-nested.md
@@ -0,0 +1,30 @@
+
+
+[Home](./index.md) > [@csstools/selector-resolve-nested](./selector-resolve-nested.md)
+
+## selector-resolve-nested package
+
+Resolve nested selectors following the CSS nesting specification.
+
+## Example
+
+
+```js
+import { resolveNestedSelector } from '@csstools/selector-resolve-nested';
+import parser from 'postcss-selector-parser';
+
+const selector = parser().astSync('.foo &');
+const parent = parser().astSync('.bar');
+
+// .foo .bar
+console.log(
+ resolveNestedSelector(selector, parent).toString()
+)
+```
+
+## Functions
+
+| Function | Description |
+| --- | --- |
+| [resolveNestedSelector(selector, parentSelector)](./selector-resolve-nested.resolvenestedselector.md) | Resolve a nested selector against a given parent selector. |
+
diff --git a/packages/selector-resolve-nested/docs/selector-resolve-nested.resolvenestedselector.md b/packages/selector-resolve-nested/docs/selector-resolve-nested.resolvenestedselector.md
new file mode 100644
index 000000000..f77747188
--- /dev/null
+++ b/packages/selector-resolve-nested/docs/selector-resolve-nested.resolvenestedselector.md
@@ -0,0 +1,27 @@
+
+
+[Home](./index.md) > [@csstools/selector-resolve-nested](./selector-resolve-nested.md) > [resolveNestedSelector](./selector-resolve-nested.resolvenestedselector.md)
+
+## resolveNestedSelector() function
+
+Resolve a nested selector against a given parent selector.
+
+**Signature:**
+
+```typescript
+export declare function resolveNestedSelector(selector: Root, parentSelector: Root): Root;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| selector | Root | The selector to resolve. |
+| parentSelector | Root | The parent selector to resolve against. |
+
+**Returns:**
+
+Root
+
+The resolved selector.
+
diff --git a/packages/selector-resolve-nested/package.json b/packages/selector-resolve-nested/package.json
new file mode 100644
index 000000000..7d4f42815
--- /dev/null
+++ b/packages/selector-resolve-nested/package.json
@@ -0,0 +1,79 @@
+{
+ "name": "@csstools/selector-resolve-nested",
+ "description": "Resolve nested CSS selectors",
+ "version": "0.0.0",
+ "contributors": [
+ {
+ "name": "Antonio Laguna",
+ "email": "antonio@laguna.es",
+ "url": "https://antonio.laguna.es"
+ },
+ {
+ "name": "Romain Menke",
+ "email": "romainmenke@gmail.com"
+ }
+ ],
+ "license": "MIT-0",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "type": "module",
+ "main": "dist/index.cjs",
+ "module": "dist/index.mjs",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.mjs"
+ },
+ "require": {
+ "default": "./dist/index.cjs"
+ }
+ }
+ },
+ "files": [
+ "CHANGELOG.md",
+ "LICENSE.md",
+ "README.md",
+ "dist"
+ ],
+ "peerDependencies": {
+ "postcss-selector-parser": "^6.0.13"
+ },
+ "devDependencies": {
+ "postcss-selector-parser": "^6.0.13"
+ },
+ "scripts": {
+ "build": "rollup -c ../../rollup/default.mjs",
+ "docs": "node ../../.github/bin/generate-docs/api-documenter.mjs",
+ "lint": "node ../../.github/bin/format-package-json.mjs",
+ "prepublishOnly": "npm run build && npm run test",
+ "stryker": "stryker run --logLevel error",
+ "test": "node --test ./test/index.mjs ./test/_import.mjs && node ./test/_require.cjs"
+ },
+ "homepage": "https://github.com/csstools/postcss-plugins/tree/main/packages/selector-resolve-nested#readme",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/csstools/postcss-plugins.git",
+ "directory": "packages/selector-resolve-nested"
+ },
+ "bugs": "https://github.com/csstools/postcss-plugins/issues",
+ "keywords": [
+ "css",
+ "postcss-selector-parser",
+ "nested"
+ ],
+ "volta": {
+ "extends": "../../package.json"
+ }
+}
diff --git a/packages/selector-resolve-nested/src/compound-selector-order.ts b/packages/selector-resolve-nested/src/compound-selector-order.ts
new file mode 100644
index 000000000..6648fda20
--- /dev/null
+++ b/packages/selector-resolve-nested/src/compound-selector-order.ts
@@ -0,0 +1,90 @@
+import parser from 'postcss-selector-parser';
+import type { Container, Node } from 'postcss-selector-parser';
+
+export function sortCompoundSelectorsInsideComplexSelector(node: Container) {
+ const compoundSelectors: Array> = [];
+ let currentCompoundSelector: Array = [];
+
+ node.each((childNode) => {
+ if (childNode.type === 'combinator') {
+ // Push
+ // - the current compound selector
+ // - the combinator
+ compoundSelectors.push(
+ currentCompoundSelector,
+ [childNode],
+ );
+
+ // Start a new compound selector
+ currentCompoundSelector = [];
+ return;
+ }
+
+ if (parser.isPseudoElement(childNode)) {
+ // Push the current compound selector
+ compoundSelectors.push(currentCompoundSelector);
+
+ // Start a new compound selector with the pseudo element as the first element
+ currentCompoundSelector = [childNode];
+ return;
+ }
+
+ if (childNode.type === 'universal' && currentCompoundSelector.find(x => x.type === 'universal')) {
+ childNode.remove();
+ return;
+ }
+
+ if (childNode.type === 'tag' && currentCompoundSelector.find(x => x.type === 'tag')) {
+ childNode.remove();
+
+ const isPseudoClone = parser.pseudo({ value: ':is' });
+ isPseudoClone.append(parser.selector({
+ nodes: [childNode],
+ value: '',
+ }));
+
+ currentCompoundSelector.push(isPseudoClone);
+ return;
+ }
+
+ currentCompoundSelector.push(childNode);
+ });
+
+ compoundSelectors.push(currentCompoundSelector);
+
+ const sortedCompoundSelectors = [];
+ for (let i = 0; i < compoundSelectors.length; i++) {
+ const compoundSelector = compoundSelectors[i];
+ compoundSelector.sort((a, b) => {
+ return selectorTypeOrder(a) - selectorTypeOrder(b);
+ });
+
+ sortedCompoundSelectors.push(...compoundSelector);
+ }
+
+ node.removeAll();
+ for (let i = sortedCompoundSelectors.length - 1; i >= 0; i--) {
+ sortedCompoundSelectors[i].remove();
+ node.prepend(sortedCompoundSelectors[i]);
+ }
+}
+
+function selectorTypeOrder(selector: Node): number {
+ if (parser.isPseudoElement(selector)) {
+ return selectorTypeOrderIndex.pseudoElement;
+ }
+
+ return selectorTypeOrderIndex[selector.type as keyof typeof selectorTypeOrderIndex];
+}
+
+const selectorTypeOrderIndex = {
+ universal: 0,
+ tag: 1,
+ pseudoElement: 2,
+ nesting: 3,
+ id: 4,
+ class: 5,
+ attribute: 6,
+ pseudo: 7,
+ comment: 8,
+};
diff --git a/packages/selector-resolve-nested/src/index.ts b/packages/selector-resolve-nested/src/index.ts
new file mode 100644
index 000000000..57def7d6b
--- /dev/null
+++ b/packages/selector-resolve-nested/src/index.ts
@@ -0,0 +1,118 @@
+/**
+ * Resolve nested selectors following the CSS nesting specification.
+ *
+ * @example
+ *
+ * ```js
+ * import { resolveNestedSelector } from '@csstools/selector-resolve-nested';
+ * import parser from 'postcss-selector-parser';
+ *
+ * const selector = parser().astSync('.foo &');
+ * const parent = parser().astSync('.bar');
+ *
+ * // .foo .bar
+ * console.log(
+ * resolveNestedSelector(selector, parent).toString()
+ * )
+ * ```
+ *
+ * @packageDocumentation
+ */
+
+import type { Container, Node, Root, Selector } from 'postcss-selector-parser';
+import parser from 'postcss-selector-parser';
+import { sortCompoundSelectorsInsideComplexSelector } from './compound-selector-order';
+
+/**
+ * Resolve a nested selector against a given parent selector.
+ *
+ * @param selector - The selector to resolve.
+ * @param parentSelector - The parent selector to resolve against.
+ * @returns The resolved selector.
+ */
+export function resolveNestedSelector(selector: Root, parentSelector: Root): Root {
+ const result: Array = [];
+
+ for (let x = 0; x < selector.nodes.length; x++) {
+ const selectorAST = selector.nodes[x].clone();
+
+ {
+ let isNestContaining = false;
+ selectorAST.walkNesting(() => {
+ isNestContaining = true;
+ return false;
+ });
+
+ if (!isNestContaining) {
+ selectorAST.prepend(parser.combinator({ value: ' ' }));
+ selectorAST.prepend(parser.nesting({}));
+ } else if (selectorAST.nodes[0]?.type === 'combinator') {
+ selectorAST.prepend(parser.nesting({}));
+ }
+ }
+
+ {
+ const needsSorting = new Set>();
+
+ selectorAST.walkNesting((node) => {
+ const parent = node.parent!;
+
+ needsSorting.add(parent);
+ if (parent.parent?.type === 'pseudo' && parent.parent.value?.toLowerCase() === ':has') {
+ node.replaceWith(...prepareParentSelectors(parentSelector, true));
+ } else {
+ node.replaceWith(...prepareParentSelectors(parentSelector));
+ }
+ });
+
+ for (const parent of needsSorting) {
+ sortCompoundSelectorsInsideComplexSelector(parent);
+ }
+ }
+
+ selectorAST.walk((node) => {
+ if (node.type === 'combinator' && node.value.trim() !== '') {
+ node.rawSpaceAfter = ' ';
+ node.rawSpaceBefore = ' ';
+ } else {
+ node.rawSpaceAfter = '';
+ node.rawSpaceBefore = '';
+ }
+ });
+
+ result.push(selectorAST);
+ }
+
+ return parser.root({
+ nodes: result,
+ value: '',
+ });
+}
+
+function prepareParentSelectors(parentSelectors: Root, forceIsPseudo: boolean = false) {
+ if (
+ forceIsPseudo ||
+ !isCompoundSelector(parentSelectors.nodes)
+ ) {
+ return [
+ parser.pseudo({
+ value: ':is',
+ nodes: parentSelectors.nodes.map((x) => x.clone()),
+ }),
+ ];
+ }
+
+ return parentSelectors.nodes[0].nodes.map((x) => x.clone());
+}
+
+function isCompoundSelector(selectors: Array): boolean {
+ // Selector list with multiple entries is not only a compound selector
+ if (selectors.length !== 1) {
+ return false;
+ }
+
+ // A selector with combinators or pseudo elements is not a compound selector
+ return !selectors[0].nodes.some((x) => {
+ return x.type === 'combinator' || parser.isPseudoElement(x);
+ });
+}
diff --git a/packages/selector-resolve-nested/stryker.conf.json b/packages/selector-resolve-nested/stryker.conf.json
new file mode 100644
index 000000000..221c75ecd
--- /dev/null
+++ b/packages/selector-resolve-nested/stryker.conf.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "../../node_modules/@stryker-mutator/core/schema/stryker-schema.json",
+ "mutate": [
+ "src/**/*.ts"
+ ],
+ "buildCommand": "rollup -c ../../rollup/default.mjs",
+ "testRunner": "command",
+ "coverageAnalysis": "perTest",
+ "tempDirName": "../../.stryker-tmp",
+ "commandRunner": {
+ "command": "node ./test/index.mjs"
+ },
+ "thresholds": {
+ "high": 100,
+ "low": 100,
+ "break": 100
+ },
+ "inPlace": true
+}
diff --git a/packages/selector-resolve-nested/test/_import.mjs b/packages/selector-resolve-nested/test/_import.mjs
new file mode 100644
index 000000000..8f0381ff3
--- /dev/null
+++ b/packages/selector-resolve-nested/test/_import.mjs
@@ -0,0 +1,8 @@
+import assert from 'assert';
+import { resolveNestedSelector } from '@csstools/selector-resolve-nested';
+import parser from 'postcss-selector-parser';
+
+const a = parser().astSync('.foo &');
+const b = parser().astSync('.bar');
+
+assert.equal(resolveNestedSelector(a, b), '.foo .bar');
diff --git a/packages/selector-resolve-nested/test/_require.cjs b/packages/selector-resolve-nested/test/_require.cjs
new file mode 100644
index 000000000..be1cf34f0
--- /dev/null
+++ b/packages/selector-resolve-nested/test/_require.cjs
@@ -0,0 +1,8 @@
+const assert = require('assert');
+const { resolveNestedSelector } = require('@csstools/selector-resolve-nested');
+const parser = require('postcss-selector-parser');
+
+const a = parser().astSync('.foo');
+const b = parser().astSync('.bar');
+
+assert.equal(resolveNestedSelector(a, b), '.bar .foo');
diff --git a/packages/selector-resolve-nested/test/complex.mjs b/packages/selector-resolve-nested/test/complex.mjs
new file mode 100644
index 000000000..d0d518cf4
--- /dev/null
+++ b/packages/selector-resolve-nested/test/complex.mjs
@@ -0,0 +1,101 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { resolveNestedSelector } from '@csstools/selector-resolve-nested';
+import { parse } from './util/parse.mjs';
+
+test('surrounding whitespace', async () => {
+ const testCases = [
+ {
+ a: '.foo + .fooz',
+ b: '.bar + .baz',
+ expected: ':is(.bar + .baz) .foo + .fooz',
+ },
+ {
+ a: '.foo+.fooz',
+ b: '.bar+.baz',
+ expected: ':is(.bar + .baz) .foo + .fooz',
+ },
+ {
+ a: '+ &',
+ b: '.bar + .baz',
+ expected: ':is(.bar + .baz) + :is(.bar + .baz)',
+ },
+ {
+ a: '+&',
+ b: '.bar +.baz',
+ expected: ':is(.bar + .baz) + :is(.bar + .baz)',
+ },
+ {
+ a: '.foo + &',
+ b: '.bar + .baz',
+ expected: '.foo + :is(.bar + .baz)',
+ },
+ {
+ a: '& + .foo',
+ b: '.bar + .baz',
+ expected: ':is(.bar + .baz) + .foo',
+ },
+ {
+ a: '& .foo',
+ b: '.bar .baz',
+ expected: ':is(.bar .baz) .foo',
+ },
+ {
+ a: '& .foo, .fooz',
+ b: '.bar .baz',
+ expected: ':is(.bar .baz) .foo,:is(.bar .baz) .fooz',
+ },
+ {
+ a: ':has(&)',
+ b: '.bar',
+ expected: ':has(:is(.bar))',
+ },
+ {
+ a: ':has(&)',
+ b: '.bar .baz',
+ expected: ':has(:is(.bar .baz))',
+ },
+ {
+ a: '.baz, &, :where(.bar, &, &:is(.foo, &, &:has(.fooz, &)))',
+ b: '.a',
+ expected: '.a .baz,.a,:where(.bar,.a,.a:is(.foo,.a,.a:has(.fooz,:is(.a))))',
+ },
+ {
+ a: 'div&::before',
+ b: '.bar + .baz',
+ expected: 'div:is(.bar + .baz)::before',
+ },
+ {
+ a: 'div::before&',
+ b: '.bar + .baz',
+ expected: 'div::before:is(.bar + .baz)',
+ },
+ {
+ a: '&',
+ b: '::before:hover',
+ expected: ':is(::before:hover)',
+ },
+ {
+ a: '&,.foo',
+ b: '',
+ expected: ', .foo',
+ },
+ {
+ a: ':is(&)',
+ b: '',
+ expected: ':is()',
+ },
+ {
+ a: '',
+ b: '.a',
+ expected: '.a ',
+ },
+ ];
+
+ for (const { a, b, expected } of testCases) {
+ assert.equal(
+ resolveNestedSelector(parse(a), parse(b)).toString(),
+ expected,
+ );
+ }
+});
diff --git a/packages/selector-resolve-nested/test/compound.mjs b/packages/selector-resolve-nested/test/compound.mjs
new file mode 100644
index 000000000..06b5f1c01
--- /dev/null
+++ b/packages/selector-resolve-nested/test/compound.mjs
@@ -0,0 +1,146 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { resolveNestedSelector } from '@csstools/selector-resolve-nested';
+import { parse } from './util/parse.mjs';
+
+test('surrounding whitespace', async () => {
+ const testCases = [
+ {
+ a: 'input&',
+ b: ':hover',
+ expected: 'input:hover',
+ },
+ {
+ a: 'input&',
+ b: 'div',
+ expected: 'input:is(div)',
+ },
+ {
+ a: '.foo&',
+ b: ':hover',
+ expected: '.foo:hover',
+ },
+ {
+ a: '.foo&',
+ b: 'div',
+ expected: 'div.foo',
+ },
+ {
+ a: 'input:is(&, :focus)',
+ b: ':hover',
+ expected: 'input:is(:hover,:focus)',
+ },
+ {
+ a: 'input:is(&, :focus)',
+ b: 'div',
+ expected: 'input:is(div,:focus)',
+ },
+ {
+ a: '.foo:is(&, :focus)',
+ b: ':hover',
+ expected: '.foo:is(:hover,:focus)',
+ },
+ {
+ a: '.foo:is(&, :focus)',
+ b: 'div',
+ expected: '.foo:is(div,:focus)',
+ },
+ {
+ a: '&',
+ b: 'div',
+ expected: 'div',
+ },
+ {
+ a: '&&',
+ b: 'div',
+ expected: 'div:is(div)',
+ },
+ {
+ a: '&&&',
+ b: 'div',
+ expected: 'div:is(div):is(div)',
+ },
+ {
+ a: 'div&::before',
+ b: ':focus',
+ expected: 'div:focus::before',
+ },
+ {
+ a: 'div::before&',
+ b: ':focus',
+ expected: 'div::before:focus',
+ },
+ {
+ a: 'div::before&[attr]',
+ b: ':focus',
+ expected: 'div::before[attr]:focus',
+ },
+ {
+ a: 'div::before[attr]&',
+ b: ':focus',
+ expected: 'div::before[attr]:focus',
+ },
+ {
+ a: 'div&.foo&[bar]&:hover',
+ b: ':focus',
+ expected: 'div.foo[bar]:focus:focus:focus:hover',
+ },
+ {
+ a: 'div&.foo&[bar]&:hover',
+ b: '[value]',
+ expected: 'div.foo[value][value][bar][value]:hover',
+ },
+ {
+ a: 'div&.foo&[bar]&:hover',
+ b: '.a + .b',
+ expected: 'div.foo[bar]:is(.a + .b):is(.a + .b):is(.a + .b):hover',
+ },
+ {
+ a: 'div.foo[bar]:hover&&&',
+ b: ':focus',
+ expected: 'div.foo[bar]:hover:focus:focus:focus',
+ },
+ {
+ a: 'div&&&.foo[bar]:hover',
+ b: ':focus',
+ expected: 'div.foo[bar]:focus:focus:focus:hover',
+ },
+ {
+ a: '&',
+ b: '*',
+ expected: '*',
+ },
+ {
+ a: '*&',
+ b: '*',
+ expected: '*',
+ },
+ {
+ a: '&&',
+ b: '*',
+ expected: '*',
+ },
+ {
+ a: '&&&',
+ b: '*',
+ expected: '*',
+ },
+ {
+ a: '&/* comment a */&/* comment b */&',
+ b: '.foo',
+ expected: '.foo.foo.foo/* comment a *//* comment b */',
+ },
+ {
+ a: 'foo&',
+ b: '.foo',
+ expected: '#foo.foo.foo',
+ },
+ ];
+
+ for (const { a, b, expected } of testCases) {
+ assert.equal(
+ resolveNestedSelector(parse(a), parse(b)).toString(),
+ expected,
+ );
+ }
+});
diff --git a/packages/selector-resolve-nested/test/index.mjs b/packages/selector-resolve-nested/test/index.mjs
new file mode 100644
index 000000000..7c4d293e7
--- /dev/null
+++ b/packages/selector-resolve-nested/test/index.mjs
@@ -0,0 +1,5 @@
+import './complex.mjs';
+import './compound.mjs';
+import './lists.mjs';
+import './simple-compound-complex.mjs';
+import './multiple-resolves.mjs';
diff --git a/packages/selector-resolve-nested/test/lists.mjs b/packages/selector-resolve-nested/test/lists.mjs
new file mode 100644
index 000000000..6b32468bb
--- /dev/null
+++ b/packages/selector-resolve-nested/test/lists.mjs
@@ -0,0 +1,49 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { resolveNestedSelector } from '@csstools/selector-resolve-nested';
+import { parse } from './util/parse.mjs';
+
+test('simple - list', async () => {
+ assert.equal(
+ resolveNestedSelector(parse('.foo'), parse('.bar, baz')).toString(),
+ ':is(.bar,baz) .foo',
+ );
+});
+
+test('relative - list', async () => {
+ assert.equal(
+ resolveNestedSelector(parse('> .foo'), parse('.bar, baz')).toString(),
+ ':is(.bar,baz) > .foo',
+ );
+});
+
+test('list - simple', async () => {
+ assert.equal(
+ resolveNestedSelector(parse('.foo, fooz'), parse('.bar')).toString(),
+ '.bar .foo,.bar fooz',
+ );
+});
+
+test('list - list', async () => {
+ assert.equal(
+ resolveNestedSelector(parse('.foo, .fooz'), parse('.bar, .baz')).toString(),
+ ':is(.bar,.baz) .foo,:is(.bar,.baz) .fooz',
+ );
+
+ assert.equal(
+ resolveNestedSelector(parse('.foo, .fooz, .foos'), parse('.bar, .baz, .bas')).toString(),
+ ':is(.bar,.baz,.bas) .foo,:is(.bar,.baz,.bas) .fooz,:is(.bar,.baz,.bas) .foos',
+ );
+
+ assert.equal(
+ resolveNestedSelector(parse('& & .foo, & & .fooz, & & .foos'), parse('.bar > :hover, .baz > :hover, .bas')).toString(),
+ ':is(.bar > :hover,.baz > :hover,.bas) :is(.bar > :hover,.baz > :hover,.bas) .foo,:is(.bar > :hover,.baz > :hover,.bas) :is(.bar > :hover,.baz > :hover,.bas) .fooz,:is(.bar > :hover,.baz > :hover,.bas) :is(.bar > :hover,.baz > :hover,.bas) .foos',
+ );
+});
+
+test('relative list - simple', async () => {
+ assert.equal(
+ resolveNestedSelector(parse('> .foo, + fooz'), parse('.bar')).toString(),
+ '.bar > .foo,.bar + fooz',
+ );
+});
diff --git a/packages/selector-resolve-nested/test/multiple-resolves.mjs b/packages/selector-resolve-nested/test/multiple-resolves.mjs
new file mode 100644
index 000000000..84057a046
--- /dev/null
+++ b/packages/selector-resolve-nested/test/multiple-resolves.mjs
@@ -0,0 +1,21 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { resolveNestedSelector } from '@csstools/selector-resolve-nested';
+import parser from 'postcss-selector-parser';
+
+test('multiple resolves', async () => {
+ const a = parser().astSync('.a .aa');
+ const b = parser().astSync('& + .b');
+ const c = parser().astSync('.c, &');
+
+ const a2 = parser().astSync('.a2 .aa2');
+
+ const cb = resolveNestedSelector(c, b);
+ assert.equal(cb.toString(), ':is(& + .b) .c,:is(& + .b)');
+
+ const cba = resolveNestedSelector(cb, a);
+ assert.equal(cba.toString(), ':is(:is(.a .aa) + .b) .c,:is(:is(.a .aa) + .b)');
+
+ const cba2 = resolveNestedSelector(cb, a2);
+ assert.equal(cba2.toString(), ':is(:is(.a2 .aa2) + .b) .c,:is(:is(.a2 .aa2) + .b)');
+});
diff --git a/packages/selector-resolve-nested/test/simple-compound-complex.mjs b/packages/selector-resolve-nested/test/simple-compound-complex.mjs
new file mode 100644
index 000000000..5c969d331
--- /dev/null
+++ b/packages/selector-resolve-nested/test/simple-compound-complex.mjs
@@ -0,0 +1,77 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { resolveNestedSelector } from '@csstools/selector-resolve-nested';
+import { parse } from './util/parse.mjs';
+
+test('surrounding whitespace', async () => {
+ assert.equal(
+ resolveNestedSelector(parse(' .foo'), parse(' .bar')).toString(),
+ '.bar .foo',
+ );
+
+ assert.equal(
+ resolveNestedSelector(parse('.foo '), parse('.bar ')).toString(),
+ '.bar .foo',
+ );
+
+ assert.equal(
+ resolveNestedSelector(parse(' .foo '), parse(' .bar ')).toString(),
+ '.bar .foo',
+ );
+});
+
+test('simple - simple', async () => {
+ assert.equal(
+ resolveNestedSelector(parse('.foo'), parse('.bar')).toString(),
+ '.bar .foo',
+ );
+});
+
+test('simple - compound', async () => {
+ assert.equal(
+ resolveNestedSelector(parse('.foo'), parse('.bar:hover')).toString(),
+ '.bar:hover .foo',
+ );
+});
+
+test('compound - simple', async () => {
+ assert.equal(
+ resolveNestedSelector(parse('.foo:focus'), parse('.bar')).toString(),
+ '.bar .foo:focus',
+ );
+});
+
+test('compound - compound', async () => {
+ assert.equal(
+ resolveNestedSelector(parse('.foo:focus'), parse('.bar:hover')).toString(),
+ '.bar:hover .foo:focus',
+ );
+});
+
+test('simple - complex', async () => {
+ assert.equal(
+ resolveNestedSelector(parse('.foo'), parse('.bar + .baz')).toString(),
+ ':is(.bar + .baz) .foo',
+ );
+});
+
+test('compound - complex', async () => {
+ assert.equal(
+ resolveNestedSelector(parse('.foo:focus'), parse('.bar + .baz')).toString(),
+ ':is(.bar + .baz) .foo:focus',
+ );
+});
+
+test('complex - compound', async () => {
+ assert.equal(
+ resolveNestedSelector(parse('.foo + .fooz'), parse('.bar:hover')).toString(),
+ '.bar:hover .foo + .fooz',
+ );
+});
+
+test('complex - complex', async () => {
+ assert.equal(
+ resolveNestedSelector(parse('.foo + .fooz'), parse('.bar + .baz')).toString(),
+ ':is(.bar + .baz) .foo + .fooz',
+ );
+});
diff --git a/packages/selector-resolve-nested/test/util/parse.mjs b/packages/selector-resolve-nested/test/util/parse.mjs
new file mode 100644
index 000000000..be0807bfe
--- /dev/null
+++ b/packages/selector-resolve-nested/test/util/parse.mjs
@@ -0,0 +1,6 @@
+import parser from 'postcss-selector-parser';
+const p = parser();
+
+export function parse(selector) {
+ return p.astSync(selector);
+}
diff --git a/packages/selector-resolve-nested/tsconfig.json b/packages/selector-resolve-nested/tsconfig.json
new file mode 100644
index 000000000..c4bc02ba4
--- /dev/null
+++ b/packages/selector-resolve-nested/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "declarationDir": ".",
+ "strict": true,
+ },
+ "include": ["./src/**/*"],
+ "exclude": ["dist"],
+}
From 8783ea4f03d21d64a2870de678e2a94fedc1a508 Mon Sep 17 00:00:00 2001
From: Romain Menke
Date: Sun, 21 Jan 2024 19:24:47 +0100
Subject: [PATCH 2/2] lint
---
packages/selector-resolve-nested/package.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/selector-resolve-nested/package.json b/packages/selector-resolve-nested/package.json
index 7d4f42815..1d539b928 100644
--- a/packages/selector-resolve-nested/package.json
+++ b/packages/selector-resolve-nested/package.json
@@ -70,8 +70,8 @@
"bugs": "https://github.com/csstools/postcss-plugins/issues",
"keywords": [
"css",
- "postcss-selector-parser",
- "nested"
+ "nested",
+ "postcss-selector-parser"
],
"volta": {
"extends": "../../package.json"