Skip to content

Commit

Permalink
Add flattenNestedSelector (#1267)
Browse files Browse the repository at this point in the history
  • Loading branch information
romainmenke authored Feb 1, 2024
1 parent 34e9e28 commit 67acd34
Show file tree
Hide file tree
Showing 24 changed files with 803 additions and 170 deletions.
4 changes: 4 additions & 0 deletions packages/selector-resolve-nested/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changes to Selector Resolve Nested

## Unreleased (minor)

- Add `flattenNestedSelector` function to support more kinds of static analysis for nested selectors.

## 1.0.3

_January 25, 2024_
Expand Down
2 changes: 1 addition & 1 deletion packages/selector-resolve-nested/dist/index.cjs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"use strict";var e=require("postcss-selector-parser");function sourceFrom(e){return{sourceIndex:e.sourceIndex??0,source:e.source}}function sortCompoundSelectorsInsideComplexSelector(o){const r=[];let t=[];o.each((o=>{if("combinator"===o.type)return r.push(t,[o]),void(t=[]);if(e.isPseudoElement(o))return r.push(t),void(t=[o]);if("universal"===o.type&&t.find((e=>"universal"===e.type)))o.remove();else{if("tag"===o.type&&t.find((e=>"tag"===e.type))){o.remove();const r=e.selector({value:"",...sourceFrom(o)});r.append(o);const n=e.pseudo({value:":is",...sourceFrom(o)});return n.append(r),void t.push(n)}t.push(o)}})),r.push(t);const n=[];for(let e=0;e<r.length;e++){const o=r[e];o.sort(((e,o)=>selectorTypeOrder(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(r){return e.isPseudoElement(r)?o.pseudoElement:o[r.type]}const o={universal:0,tag:1,pseudoElement:2,nesting:3,id:4,class:5,attribute:6,pseudo:7,comment:8};function prepareParentSelectors(o,r=!1){if(r||!isCompoundSelector(o.nodes)){const r=e.pseudo({value:":is",...sourceFrom(o)});return o.nodes.forEach((e=>{r.append(e.clone())})),[r]}return 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)))}exports.resolveNestedSelector=function resolveNestedSelector(o,r){const t=[];for(let n=0;n<o.nodes.length;n++){const s=o.nodes[n].clone();{let o=!1;s.walkNesting((()=>(o=!0,!1))),o?"combinator"===s.nodes[0]?.type&&s.prepend(e.nesting({...sourceFrom(s)})):(s.prepend(e.combinator({value:" ",...sourceFrom(s)})),s.prepend(e.nesting({...sourceFrom(s)})))}{const e=new Set;s.walkNesting((o=>{const t=o.parent;e.add(t),"pseudo"===t.parent?.type&&":has"===t.parent.value?.toLowerCase()?o.replaceWith(...prepareParentSelectors(r,!0)):o.replaceWith(...prepareParentSelectors(r))}));for(const o of e)sortCompoundSelectorsInsideComplexSelector(o)}s.walk((e=>{"combinator"===e.type&&""!==e.value.trim()?(e.rawSpaceAfter=" ",e.rawSpaceBefore=" "):(e.rawSpaceAfter="",e.rawSpaceBefore="")})),t.push(s)}const n=e.root({value:"",...sourceFrom(o)});return t.forEach((e=>{n.append(e)})),n};
"use strict";var e=require("postcss-selector-parser");function sourceFrom(e){return{sourceIndex:e.sourceIndex??0,source:e.source}}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.selector({value:"",...sourceFrom(o)});t.append(o);const n=e.pseudo({value:":is",...sourceFrom(o)});return n.append(t),void r.push(n)}r.push(o)}})),t.push(r);const n=[];for(let e=0;e<t.length;e++){const o=t[e];o.sort(((e,o)=>selectorTypeOrder(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 prepareParentSelectors(o,t=!1){if(t||!isCompoundSelector(o.nodes)){const t=e.pseudo({value:":is",...sourceFrom(o)});return o.nodes.forEach((e=>{t.append(e.clone())})),[t]}return 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)))}function combinationsWithSizeN(e,o){if(o<2)throw new Error("n must be greater than 1");if(e.length<2)throw new Error("s must be greater than 1");if(Math.pow(e.length,o)>1e4)throw new Error("Too many combinations when trying to resolve a nested selector with lists, reduce the complexity of your selectors");const t=[];for(let e=0;e<o;e++)t[e]=0;const r=[];for(;;){const n=[];for(let s=o-1;s>=0;s--){let o=t[s];if(o>=e.length){if(o=0,t[s]=0,0===s)return r;t[s-1]+=1}n[s]=e[o].clone()}r.push(n),t[t.length-1]++}}exports.flattenNestedSelector=function flattenNestedSelector(o,t){const r=[];for(let n=0;n<o.nodes.length;n++){const s=o.nodes[n].clone();let c,l=0;{let o=!1;s.walkNesting((()=>{o=!0,l++})),o?"combinator"===s.nodes[0]?.type&&(s.prepend(e.nesting({...sourceFrom(s)})),l++):(s.prepend(e.combinator({value:" ",...sourceFrom(s)})),s.prepend(e.nesting({...sourceFrom(s)})),l++)}let p=[];if(l>1&&t.nodes.length>1)p=combinationsWithSizeN(t.nodes,l),c=p.length;else{c=t.nodes.length;for(let e=0;e<t.nodes.length;e++){p.push([]);for(let o=0;o<l;o++)p[e].push(t.nodes[e].clone())}}for(let e=0;e<c;e++){let o=0;const t=s.clone();t.walkNesting((t=>{const r=p[e][o];o++,t.replaceWith(...r.nodes)})),r.push(t)}}const n=e.root({value:"",...sourceFrom(o)});return r.forEach((e=>{n.append(e)})),n},exports.resolveNestedSelector=function resolveNestedSelector(o,t){const r=[];for(let n=0;n<o.nodes.length;n++){const s=o.nodes[n].clone();{let o=!1;s.walkNesting((()=>(o=!0,!1))),o?"combinator"===s.nodes[0]?.type&&s.prepend(e.nesting({...sourceFrom(s)})):(s.prepend(e.combinator({value:" ",...sourceFrom(s)})),s.prepend(e.nesting({...sourceFrom(s)})))}{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)}const n=e.root({value:"",...sourceFrom(o)});return r.forEach((e=>{n.append(e)})),n};
14 changes: 14 additions & 0 deletions packages/selector-resolve-nested/dist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@

import type { Root } from 'postcss-selector-parser';

/**
* Flatten a nested selector against a given parent selector.
*
* ⚠️ This is not a method to generate the equivalent un-nested selector.
* It is purely a method to construct a single selector AST that contains the parts of both the current and parent selector.
* It will not have the correct specificity and it will not match the right elements when used as a selector.
* It will not always serialize to a valid selector.
*
* @param selector - The selector to resolve.
* @param parentSelector - The parent selector to resolve against.
* @returns The resolved selector.
*/
export declare function flattenNestedSelector(selector: Root, parentSelector: Root): Root;

/**
* Resolve a nested selector against a given parent selector.
*
Expand Down
2 changes: 1 addition & 1 deletion packages/selector-resolve-nested/dist/index.mjs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
import e from"postcss-selector-parser";function sourceFrom(e){return{sourceIndex:e.sourceIndex??0,source:e.source}}function sortCompoundSelectorsInsideComplexSelector(o){const r=[];let t=[];o.each((o=>{if("combinator"===o.type)return r.push(t,[o]),void(t=[]);if(e.isPseudoElement(o))return r.push(t),void(t=[o]);if("universal"===o.type&&t.find((e=>"universal"===e.type)))o.remove();else{if("tag"===o.type&&t.find((e=>"tag"===e.type))){o.remove();const r=e.selector({value:"",...sourceFrom(o)});r.append(o);const n=e.pseudo({value:":is",...sourceFrom(o)});return n.append(r),void t.push(n)}t.push(o)}})),r.push(t);const n=[];for(let e=0;e<r.length;e++){const o=r[e];o.sort(((e,o)=>selectorTypeOrder(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(r){return e.isPseudoElement(r)?o.pseudoElement:o[r.type]}const o={universal:0,tag:1,pseudoElement:2,nesting:3,id:4,class:5,attribute:6,pseudo:7,comment:8};function resolveNestedSelector(o,r){const t=[];for(let n=0;n<o.nodes.length;n++){const s=o.nodes[n].clone();{let o=!1;s.walkNesting((()=>(o=!0,!1))),o?"combinator"===s.nodes[0]?.type&&s.prepend(e.nesting({...sourceFrom(s)})):(s.prepend(e.combinator({value:" ",...sourceFrom(s)})),s.prepend(e.nesting({...sourceFrom(s)})))}{const e=new Set;s.walkNesting((o=>{const t=o.parent;e.add(t),"pseudo"===t.parent?.type&&":has"===t.parent.value?.toLowerCase()?o.replaceWith(...prepareParentSelectors(r,!0)):o.replaceWith(...prepareParentSelectors(r))}));for(const o of e)sortCompoundSelectorsInsideComplexSelector(o)}s.walk((e=>{"combinator"===e.type&&""!==e.value.trim()?(e.rawSpaceAfter=" ",e.rawSpaceBefore=" "):(e.rawSpaceAfter="",e.rawSpaceBefore="")})),t.push(s)}const n=e.root({value:"",...sourceFrom(o)});return t.forEach((e=>{n.append(e)})),n}function prepareParentSelectors(o,r=!1){if(r||!isCompoundSelector(o.nodes)){const r=e.pseudo({value:":is",...sourceFrom(o)});return o.nodes.forEach((e=>{r.append(e.clone())})),[r]}return 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};
import e from"postcss-selector-parser";function sourceFrom(e){return{sourceIndex:e.sourceIndex??0,source:e.source}}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.selector({value:"",...sourceFrom(o)});t.append(o);const n=e.pseudo({value:":is",...sourceFrom(o)});return n.append(t),void r.push(n)}r.push(o)}})),t.push(r);const n=[];for(let e=0;e<t.length;e++){const o=t[e];o.sort(((e,o)=>selectorTypeOrder(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.nodes.length;n++){const s=o.nodes[n].clone();{let o=!1;s.walkNesting((()=>(o=!0,!1))),o?"combinator"===s.nodes[0]?.type&&s.prepend(e.nesting({...sourceFrom(s)})):(s.prepend(e.combinator({value:" ",...sourceFrom(s)})),s.prepend(e.nesting({...sourceFrom(s)})))}{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)}const n=e.root({value:"",...sourceFrom(o)});return r.forEach((e=>{n.append(e)})),n}function prepareParentSelectors(o,t=!1){if(t||!isCompoundSelector(o.nodes)){const t=e.pseudo({value:":is",...sourceFrom(o)});return o.nodes.forEach((e=>{t.append(e.clone())})),[t]}return 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)))}function combinationsWithSizeN(e,o){if(o<2)throw new Error("n must be greater than 1");if(e.length<2)throw new Error("s must be greater than 1");if(Math.pow(e.length,o)>1e4)throw new Error("Too many combinations when trying to resolve a nested selector with lists, reduce the complexity of your selectors");const t=[];for(let e=0;e<o;e++)t[e]=0;const r=[];for(;;){const n=[];for(let s=o-1;s>=0;s--){let o=t[s];if(o>=e.length){if(o=0,t[s]=0,0===s)return r;t[s-1]+=1}n[s]=e[o].clone()}r.push(n),t[t.length-1]++}}function flattenNestedSelector(o,t){const r=[];for(let n=0;n<o.nodes.length;n++){const s=o.nodes[n].clone();let c,l=0;{let o=!1;s.walkNesting((()=>{o=!0,l++})),o?"combinator"===s.nodes[0]?.type&&(s.prepend(e.nesting({...sourceFrom(s)})),l++):(s.prepend(e.combinator({value:" ",...sourceFrom(s)})),s.prepend(e.nesting({...sourceFrom(s)})),l++)}let p=[];if(l>1&&t.nodes.length>1)p=combinationsWithSizeN(t.nodes,l),c=p.length;else{c=t.nodes.length;for(let e=0;e<t.nodes.length;e++){p.push([]);for(let o=0;o<l;o++)p[e].push(t.nodes[e].clone())}}for(let e=0;e<c;e++){let o=0;const t=s.clone();t.walkNesting((t=>{const r=p[e][o];o++,t.replaceWith(...r.nodes)})),r.push(t)}}const n=e.root({value:"",...sourceFrom(o)});return r.forEach((e=>{n.append(e)})),n}export{flattenNestedSelector,resolveNestedSelector};
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,70 @@
"name": "",
"preserveMemberOrder": false,
"members": [
{
"kind": "Function",
"canonicalReference": "@csstools/selector-resolve-nested!flattenNestedSelector:function(1)",
"docComment": "/**\n * Flatten a nested selector against a given parent selector.\n *\n * ⚠️ This is not a method to generate the equivalent un-nested selector. It is purely a method to construct a single selector AST that contains the parts of both the current and parent selector. It will not have the correct specificity and it will not match the right elements when used as a selector. It will not always serialize to a valid 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 flattenNestedSelector(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/flatten-nested-selector.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": "flattenNestedSelector"
},
{
"kind": "Function",
"canonicalReference": "@csstools/selector-resolve-nested!resolveNestedSelector:function(1)",
Expand Down Expand Up @@ -208,7 +272,7 @@
"text": ";"
}
],
"fileUrlPath": "dist/_types/index.d.ts",
"fileUrlPath": "dist/_types/resolve-nested-selector.d.ts",
"returnTypeTokenRange": {
"startIndex": 5,
"endIndex": 6
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@csstools/selector-resolve-nested](./selector-resolve-nested.md) &gt; [flattenNestedSelector](./selector-resolve-nested.flattennestedselector.md)

## flattenNestedSelector() function

Flatten a nested selector against a given parent selector.

⚠️ This is not a method to generate the equivalent un-nested selector. It is purely a method to construct a single selector AST that contains the parts of both the current and parent selector. It will not have the correct specificity and it will not match the right elements when used as a selector. It will not always serialize to a valid selector.

**Signature:**

```typescript
export declare function flattenNestedSelector(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.

Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ console.log(

| Function | Description |
| --- | --- |
| [flattenNestedSelector(selector, parentSelector)](./selector-resolve-nested.flattennestedselector.md) | <p>Flatten a nested selector against a given parent selector.</p><p>⚠️ This is not a method to generate the equivalent un-nested selector. It is purely a method to construct a single selector AST that contains the parts of both the current and parent selector. It will not have the correct specificity and it will not match the right elements when used as a selector. It will not always serialize to a valid selector.</p> |
| [resolveNestedSelector(selector, parentSelector)](./selector-resolve-nested.resolvenestedselector.md) | Resolve a nested selector against a given parent selector. |

74 changes: 74 additions & 0 deletions packages/selector-resolve-nested/src/combinations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { Selector } from 'postcss-selector-parser';

export function combinationsWithSizeN(set: Array<Selector>, n: number): Array<Array<Selector>> {
// set is the list of parent selectors
// n is the amount of `&` selectors in the current selector.
// all combinations of values in the set with an array size of n must be generated to match the nesting selector behavior.
//
// for example :
// a current selector like: `& + & {}`
// with parent : `.foo, .bar {}`
//
// the set is `['.foo', '.bar']` and n is 2, the resulting combinations are:
// ['.foo', '.bar']
// ['.foo', '.foo']
// ['.bar', '.foo']
// ['.bar', '.bar']
//
// outputted like :
// .foo + .bar,
// .foo + .foo,
// .bar + .foo,
// .bar + .bar {}


/* node:coverage disable */
if (n < 2) {
// should never happen and is checked by caller
throw new Error('n must be greater than 1');
}

if (set.length < 2) {
// should never happen and is checked by caller
throw new Error('s must be greater than 1');
}

if (Math.pow(set.length, n) > 10000) {
// Throwing is best here as a warning would be impossible to handle gracefully on our end.
// This will error mid transform and there is no possible fallback at this point.
// The user should reduce complexity.
throw new Error('Too many combinations when trying to resolve a nested selector with lists, reduce the complexity of your selectors');
}
/* node:coverage enable */

const counters: Array<number> = [];

for (let i = 0; i < n; i++) {
counters[i] = 0;
}

const result: Array<Array<Selector>> = [];

// eslint-disable-next-line no-constant-condition
while (true) {
const ss: Array<Selector> = [];
for (let i = n - 1; i >= 0; i--) {
let currentCounter = counters[i];
if (currentCounter >= set.length) {
currentCounter = 0;
counters[i] = 0;

if (i === 0) {
return result;
} else {
counters[i - 1] += 1;
}
}

ss[i] = set[currentCounter].clone();
}

result.push(ss);
counters[counters.length - 1]++;
}
}
Loading

0 comments on commit 67acd34

Please sign in to comment.