├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yml │ └── dependabot-auto-merge.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Do you want to request a _feature_ or report a _bug_? 2 | 3 | ... 4 | 5 | ### What is the current behaviour? 6 | 7 | ... 8 | 9 | ### What is the expected behaviour? 10 | 11 | ... 12 | 13 | ### Steps to Reproduce the Problem 14 | 15 | 1. ... 16 | 2. ... 17 | 3. ... 18 | 19 | ### Specifications 20 | 21 | - Version: 22 | - Platform: 23 | - Subsystem: 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What kind of change does this PR introduce? _(Bug fix, feature, docs update, ...)_ 2 | 3 | ... 4 | 5 | ### What is the current behaviour? _(You can also link to an open issue here)_ 6 | 7 | ... 8 | 9 | ### What is the new behaviour? 10 | 11 | ... 12 | 13 | ### Does this PR introduce a breaking change? _(What changes might users need to make in their application due to this PR?)_ 14 | 15 | ... 16 | 17 | ### Other information: 18 | 19 | ### Please check if the PR fulfills these requirements: 20 | 21 | - [ ] Tests for the changes have been added 22 | - [ ] Docs have been added / updated 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | 8 | - package-ecosystem: 'github-actions' 9 | directory: '/' 10 | schedule: 11 | interval: 'daily' 12 | groups: 13 | upload-download-artifact: 14 | patterns: 15 | - 'actions/upload-artifact' 16 | - 'actions/download-artifact' 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | env: 9 | CI: true 10 | 11 | jobs: 12 | test-and-build: 13 | name: 'Test and build' 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: actions/setup-node@v4 20 | with: 21 | node-version-file: '.nvmrc' 22 | 23 | - run: npm ci 24 | 25 | - uses: actions/upload-artifact@v4 26 | with: 27 | name: code-coverage 28 | path: coverage 29 | 30 | upload-code-coverage: 31 | name: 'Upload code coverage' 32 | needs: ['test-and-build'] 33 | runs-on: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | 38 | - uses: actions/download-artifact@v4 39 | with: 40 | name: code-coverage 41 | path: coverage 42 | 43 | - uses: coverallsapp/github-action@v2 44 | with: 45 | github-token: ${{ secrets.GITHUB_TOKEN }} 46 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto merge 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | dependabot-auto-merge: 8 | name: 'Dependabot auto merge' 9 | runs-on: ubuntu-latest 10 | permissions: 11 | pull-requests: write 12 | contents: write 13 | steps: 14 | - uses: fastify/github-action-merge-dependabot@v3 15 | with: 16 | target: minor 17 | use-github-auto-merge: true 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | .nyc_output 4 | coverage 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.20.2 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | package.json 3 | package-lock.json 4 | .nyc_output 5 | coverage 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.0.0 2 | 3 | Breaking Changes 4 | 5 | - Use `PostCSS` 8 6 | - Node supported version >=12 7 | 8 | ## 2.0.0 9 | 10 | - Consider `rule` nodes only when when building ancestor selectors 11 | - Use `PostCSS` 6 12 | - Restrict support to `node.js` >= 4 13 | - Remove `object-assign` dependency 14 | 15 | ## 1.0.0 16 | 17 | - Solve complex nesting scenarios scenarios externalizing parent selectors resolution to [postcss-resolve-nested-selector](https://github.com/davidtheclark/) 18 | - Refactor bootstrap function using `walkRules` and `walkDecls` PostCSS methods 19 | - `replaceDeclarations` option will replace declaration values only 20 | 21 | ## 0.1.0 22 | 23 | - Add experimental `replaceDeclarations` option, to process declaration props and values, too 24 | - Cast warning when nestingLevel >= parentsStack.length 25 | - Move `spacesAndAmpersandRegex` regex into a reusable regex 26 | - Add a failing test case documenting issues when complex nesting 27 | 28 | ## 0.0.1 29 | 30 | - Fix `levelSymbol` and `parentSymbol` options not being used 31 | 32 | ## 0.0.0 33 | 34 | - Initial release 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2016 Andrea Carraro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostCSS Nested ancestors 2 | 3 | [![Build status][ci-badge]][ci] 4 | [![Npm version][npm-version-badge]][npm] 5 | [![Test coverage report][coveralls-badge]][coveralls] 6 | 7 | [PostCSS] plugin to reference any parent ancestor selector in nested CSS. 8 | 9 | ## Getting ancestor selectors 10 | 11 | When writing modular nested CSS, `&` current parent selector is often not enough. 12 | 13 | **PostCSS Nested ancestors** introduces `^&` selector which let you reference **any parent ancestor selector** with an easy and customizable interface. 14 | 15 | This plugin should be used **before** a PostCSS rules unwrapper like [postcss-nested]. 16 | 17 | See [PostCSS] docs for examples for your environment. 18 | 19 | ### Ancestor selectors schema 20 | 21 | ``` 22 | .level-1 { 23 | | | .level-2 { 24 | | | | .level-3 { 25 | | | | | .level-4 { 26 | | | | | | 27 | | | | | --- & {} /* & = ".level-1 .level-2 .level-3 .level-4" */ 28 | | | | ------- ^& {} /* ^& = ".level-1 .level-2 .level-3" */ 29 | | | ----------- ^^& {} /* ^^& = ".level-1 .level-2" */ 30 | | --------------- ^^^& {} /* ^^^& = ".level-1" */ 31 | ------------------- ^^^^& {} /* ^^^^& = "" */ 32 | } 33 | } 34 | } 35 | } 36 | ``` 37 | 38 | ### A real example 39 | 40 | ```css 41 | /* Without postcss-nested-ancestors */ 42 | .MyComponent 43 | &-part{} 44 | &:hover { 45 | > .MyComponent-part {} /* Must manually repeat ".MyComponent" for each child */ 46 | } 47 | } 48 | 49 | /* With postcss-nested-ancestors */ 50 | .MyComponent 51 | &-part{} 52 | &:hover { 53 | > ^&-part {} /* Skip ":hover" inheritance here */ 54 | } 55 | } 56 | 57 | /* After postcss-nested-ancestors */ 58 | .MyComponent { 59 | &-part{} 60 | &:hover { 61 | > .MyComponent-part {} 62 | } 63 | 64 | /* After postcss-nested */ 65 | .MyComponent {} 66 | .MyComponent-part {} 67 | .MyComponent:hover {} 68 | .MyComponent:hover > .MyComponent-part {} /* No ":hover" inheritance here! */ 69 | 70 | ``` 71 | 72 | ## Why? 73 | 74 | Currently another plugin - [postcss-current-selector] - has tried to solve the problem of referencing ancestors selector. It works great, but its approach involves assigning ancestor selectors to special variables to be later processed by a further postcss plugin [postcss-simple-vars]. 75 | 76 | **PostCSS Nested ancestors** instead replaces special ancestor selectors, makes no use of variable assignment and produces an output ready to be unwrapped with [postcss-nested]. 77 | 78 | ## Installation 79 | 80 | ```console 81 | $ npm install --save-dev postcss postcss-nested-ancestors 82 | ``` 83 | 84 | ## Usage 85 | 86 | ```js 87 | postcss([require('postcss-nested-ancestors')]); 88 | ``` 89 | 90 | ## Options 91 | 92 | ### placeholder 93 | 94 | Type: `string` 95 | Default: `^&` 96 | 97 | Ancestor selector pattern (utility option to automatically set both `levelSymbol` and `parentSymbol`) 98 | 99 | ### levelSymbol 100 | 101 | Type: `string` 102 | Default: `^` 103 | 104 | Define ancestor selector fragment relative to the matching nesting level 105 | 106 | ### parentSymbol 107 | 108 | Type: `string` 109 | Default: `&` 110 | 111 | Ancestor selector base symbol 112 | 113 | ### replaceDeclarations (experimental) 114 | 115 | Type: `boolean` 116 | Default: `false` 117 | 118 | If this is true then this plugin will look through your declaration values for the placeholder symbol and replace them with specified selector. 119 | 120 | An use case for this if enabling [postcss-ref](https://github.com/morishitter/postcss-ref) to work with dynamic `@ref` selectors. Read discussion [here](https://github.com/toomuchdesign/postcss-nested-ancestors/pull/3). 121 | 122 | ```css 123 | /* Before */ 124 | .foo { 125 | &:last-child { 126 | border-top: ref(^&, border-bottom); 127 | } 128 | } 129 | 130 | /* After PostCSS Nested ancestors and PostCSS Nested */ 131 | .foo { 132 | } 133 | 134 | .foo:last-child { 135 | border-top: ref(.foo, border-bottom); 136 | } 137 | ``` 138 | 139 | ## Known issues 140 | 141 | ### Multiple ancestor placeholders in same selector 142 | 143 | This plugin currently fails when trying to replace **more than one different ancestor placeholder in a single rule selector**. This scenario has not been considered in order to not bloat the code with a remote use case. 144 | 145 | More precisely, all ancestor placeholders are replaced, but processed as if they where the equal to the first ancestor placeholder found in selector. 146 | 147 | In general, **do not use more than one ancestor placeholder in a single rule selector**. Anyway, this use case can be rewritten by **splitting the selectors in multiple nested rules** (see edge case 2). 148 | 149 | #### Edge case 1 (success) 150 | 151 | ```css 152 | /* 2 equal ancestor placeholders in single rule selector */ 153 | .a { 154 | &:hover { 155 | ^&^&-b { 156 | } 157 | } 158 | } 159 | 160 | /* Output: It works but casts a warning */ 161 | .a { 162 | &:hover { 163 | .a.a-b { 164 | } 165 | } 166 | } 167 | ``` 168 | 169 | #### Edge case 2 (failing) 170 | 171 | ```css 172 | /* 2 different ancestor placeholders in single rule selector */ 173 | .a { 174 | &-b { 175 | &:hover { 176 | /* Will be processed as ^&^&-c{}, sorry! */ 177 | ^&^^&-c { 178 | } 179 | } 180 | } 181 | } 182 | 183 | /* Wrong output: All placeholder replaced with the value of the first one */ 184 | .a { 185 | &-b { 186 | &:hover { 187 | /* Expected output: .a-b.a-c{}*/ 188 | .a-b.a-b-c { 189 | } 190 | } 191 | } 192 | } 193 | 194 | /* This use case can be rewritten as: */ 195 | .a { 196 | &-b { 197 | &:hover { 198 | ^& { 199 | &^^^&-c { 200 | } 201 | } 202 | } 203 | } 204 | } 205 | ``` 206 | 207 | ### Replace declaration values in complex nesting scenarios 208 | 209 | `replaceDeclarations` options used in a complex nesting scenario might have undesired outputs because of the different nature of CSS selectors and and declaration values. 210 | 211 | In general, avoid replacing declaration values when inside a rule with multiple selectors (but why should you?). In other words don't get yourself into trouble! 212 | 213 | Here is an example of what you don't want to do. 214 | 215 | ```css 216 | /* Don't replace declaration value inside multiple selector rules */ 217 | .a1, 218 | .a2 { 219 | &:hover { 220 | &:before { 221 | content: '^^&'; 222 | } 223 | } 224 | } 225 | 226 | /* Output */ 227 | .a1, 228 | .a2 { 229 | &:hover { 230 | &:before { 231 | content: '.a1,.a2'; 232 | } 233 | } 234 | } 235 | ``` 236 | 237 | ## Contributing 238 | 239 | Contributions are super welcome, but please follow the conventions below if you want to do a pull request: 240 | 241 | - Create a new branch and make the pull request from that branch 242 | - Each pull request for a single feature or bug fix 243 | - If you are planning on doing something big, please discuss first with [@toomuchdesign](http://www.twitter.com/toomuchdesign) about it 244 | - Update tests (`test.js`) covering new features 245 | 246 | ## Todo's 247 | 248 | - Better comment source code 249 | 250 | [postcss]: https://github.com/postcss/postcss 251 | [ci-badge]: https://github.com/toomuchdesign/postcss-nested-ancestors/actions/workflows/ci.yml/badge.svg 252 | [ci]: https://github.com/toomuchdesign/postcss-nested-ancestors/actions/workflows/ci.yml 253 | [coveralls-badge]: https://coveralls.io/repos/github/toomuchdesign/postcss-nested-ancestors/badge.svg?branch=master 254 | [coveralls]: https://coveralls.io/github/toomuchdesign/postcss-nested-ancestors?branch=master 255 | [npm]: https://www.npmjs.com/package/postcss-nested-ancestors 256 | [npm-version-badge]: https://img.shields.io/npm/v/postcss-nested-ancestors.svg 257 | [postcss-current-selector]: https://github.com/komlev/postcss-current-selector 258 | [postcss-nested]: https://github.com/postcss/postcss-nested 259 | [postcss-simple-vars]: https://github.com/postcss/postcss-simple-vars 260 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var resolvedNestedSelector = require('postcss-resolve-nested-selector'); 2 | var escRgx = require('escape-string-regexp'); 3 | 4 | /** 5 | * @type {import('postcss').PluginCreator} 6 | */ 7 | module.exports = (opts = {}) => { 8 | opts = Object.assign( 9 | { 10 | placeholder: '^&', 11 | replaceDeclarations: false, 12 | }, 13 | opts 14 | ); 15 | 16 | // Advanced options 17 | opts = Object.assign( 18 | { 19 | levelSymbol: opts.levelSymbol || opts.placeholder.charAt(0), 20 | parentSymbol: opts.parentSymbol || opts.placeholder.charAt(1), 21 | }, 22 | opts 23 | ); 24 | 25 | // Gets all ancestors placeholder recurrencies: ^&, ^^&, ^^^&, [...] 26 | var placeholderRegex = new RegExp( 27 | '(' + escRgx(opts.levelSymbol) + ')+(' + escRgx(opts.parentSymbol) + ')', 28 | 'g' 29 | ); 30 | 31 | /** 32 | * Get first parent rule node (no @-rules) 33 | * @param {Object} node PostCSS node object 34 | * @return {Object|false} Parent node or false if no parent rule found 35 | */ 36 | function getParentRule(node) { 37 | var parentNode = node.parent; 38 | 39 | if (parentNode.type === 'rule') { 40 | return parentNode; 41 | } 42 | 43 | if (parentNode.type === 'root') { 44 | return false; 45 | } 46 | 47 | return getParentRule(parentNode); 48 | } 49 | 50 | /** 51 | * Climb up PostCSS node parent stack (no @-rules) 52 | * @param {Object} node PostCSS node object 53 | * @param {Number} nestingLevel Number of parent to climb 54 | * @return {Object|false} Parent node or false if no matching parent 55 | */ 56 | function getParentRuleAtLevel(node, nestingLevel) { 57 | var currentNode = node; 58 | nestingLevel = nestingLevel || 1; 59 | 60 | for (var i = 0; i < nestingLevel; i++) { 61 | currentNode = getParentRule(currentNode); 62 | 63 | if (!currentNode) { 64 | return false; 65 | } 66 | } 67 | return currentNode; 68 | } 69 | 70 | /** 71 | * Given a PostCSS object and the level of a parent rule, 72 | * return the selector of the matching parent rule 73 | * 74 | * @param {Object} node PostCSS Node object 75 | * @param {Number} nestingLevel Ancestor nesting depth (0 = &, 1 = ^&, ...) 76 | * @param {Object} result PostCSS Result object 77 | * @return {Array} Array of ancestor selectors 78 | */ 79 | function getParentSelectorsAtLevel(node, nestingLevel, result) { 80 | nestingLevel = nestingLevel || 1; 81 | 82 | // Get parent PostCSS node object at requested nesting level 83 | var parentNodeAtLevel = getParentRuleAtLevel(node, nestingLevel + 1); 84 | 85 | // Iterate each matching parent node selectors and resolve them 86 | if (parentNodeAtLevel && parentNodeAtLevel.selectors) { 87 | return parentNodeAtLevel.selectors 88 | .map(function (selector) { 89 | // Resolve parent selectors for each node 90 | return resolvedNestedSelector(selector, parentNodeAtLevel); 91 | }) 92 | .reduce(function (a, b) { 93 | // Flatten array of arrays 94 | return a.concat(b); 95 | }); 96 | } else { 97 | // Set a warning no matching parent node found 98 | node.warn(result, 'Parent selector exceeds current stack.'); 99 | return ['']; 100 | } 101 | } 102 | 103 | /** 104 | * Given an ancestor placeholder and the PostCSS node object, 105 | * returns the corresponding parent selectors 106 | * calculated from the provided PostCSS node object. 107 | * 108 | * @param {String} placeholder Ancestor placeholder (eg.^^&) 109 | * @param {Object} node PostCSS Node object 110 | * @param {Object} result PostCSS Result object 111 | * @return {Array} Array of ancestor selectors 112 | */ 113 | function getMatchingParentSelectors(placeholder, node, result) { 114 | return getParentSelectorsAtLevel( 115 | node, 116 | // Get how many level symbols ("^") has current placeholder 117 | placeholder.lastIndexOf(opts.levelSymbol) / opts.levelSymbol.length + 1, 118 | result 119 | ); 120 | } 121 | 122 | /** 123 | * Given a PostCSS node object, 124 | * generate an array of selector from provided PostCSS node selectors 125 | * in which ancestor placeholders are replaced with actual matching parent selectors. 126 | * 127 | * In case of multiple parent selectors, the returning selectors array will 128 | * contain more items then the original one. 129 | * 130 | * @param {Object} node a PostCSS Node object 131 | * @param {Object} result a PostCSS Result object 132 | * @return {String} Array of Arrays of CSS selectors 133 | */ 134 | function getReplacedSelectors(node, result) { 135 | // Parse each singular selector 136 | const resolvedSelectors = node.selectors.map(function (selector) { 137 | // Look for ancestor placeholders into selector (eg. ^^&-foo) 138 | const placeholders = selector.match(placeholderRegex); 139 | 140 | // Ancestor placeholder found! (eg. ^^&): 141 | if (placeholders) { 142 | /* 143 | * Warning! 144 | * If more than one ancestor placeholder found (placeholders.length > 1) 145 | * in the same selector, all placeholders will be processed like 146 | * they were equal to the first one. (eg. '^&^^&' --> '^&^&'). 147 | * 148 | * It is to avoid useless complexity in a scenario which can be handled 149 | * by splitting the selector in 2 nested selectors. 150 | * (eg. '^&^^&' --> '^&{ &^^^&{}'). mmh? ;-) 151 | * 152 | */ 153 | if (placeholders.length > 1) { 154 | node.warn( 155 | result, 156 | 'More then one ancestor placeholders found in same selector.' 157 | ); 158 | } 159 | /* 160 | * Get an array of parent selectors build from found placeholder 161 | * (eg. ['.ancestor-1, '.ancestor-2']) 162 | * 163 | * See the following "placeholders[0]"" 164 | */ 165 | const parentSelectors = getMatchingParentSelectors( 166 | placeholders[0], 167 | node, 168 | result 169 | ); 170 | 171 | /* 172 | * Replace original selector string with an array of updated selectors. 173 | * The ancestor placeholder (^^&) found in original selector (^^&-foo) 174 | * is replaced by just generated parent selectors. 175 | * (eg. '^^&-foo' --> ['.ancestor-1-foo, '.ancestor-2-foo']) 176 | */ 177 | return parentSelectors.map(function (parentSelector) { 178 | return selector.replace(placeholderRegex, parentSelector); 179 | }); 180 | } 181 | // No ancestor placeholders found! Return original selector 182 | return selector; 183 | }); 184 | 185 | return resolvedSelectors; 186 | } 187 | 188 | var process = function (node, result) { 189 | // Replace parents placeholders in each rule selector 190 | node.walkRules(function (rule) { 191 | rule.selectors = getReplacedSelectors(rule, result); 192 | }); 193 | 194 | // Replace parents placeholders in each rule declaration value 195 | if (opts.replaceDeclarations) { 196 | node.walkDecls(function (decl) { 197 | decl.value = decl.value.replace( 198 | placeholderRegex, 199 | function (placeholder) { 200 | // Get parent selectors array and join it as a comma separated string 201 | return getMatchingParentSelectors(placeholder, decl, result).join(); 202 | } 203 | ); 204 | }); 205 | } 206 | }; 207 | 208 | return { 209 | postcssPlugin: 'postcss-nested-ancestors', 210 | Once(root, { result }) { 211 | process(root, result); 212 | }, 213 | }; 214 | }; 215 | 216 | module.exports.postcss = true; 217 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-nested-ancestors", 3 | "version": "3.0.0", 4 | "description": "PostCSS plugin to reference any ancestor selector in nested CSS", 5 | "engines": { 6 | "node": ">=v16.20.2" 7 | }, 8 | "files": [ 9 | "index.js" 10 | ], 11 | "keywords": [ 12 | "postcss", 13 | "css", 14 | "postcss-plugin", 15 | "ancestor", 16 | "grandparent", 17 | "selector", 18 | "postcss-nested" 19 | ], 20 | "author": "Andrea Carraro ", 21 | "license": "MIT", 22 | "repository": "toomuchdesign/postcss-nested-ancestors", 23 | "bugs": { 24 | "url": "https://github.com/toomuchdesign/postcss-nested-ancestors/issues" 25 | }, 26 | "homepage": "https://github.com/toomuchdesign/postcss-nested-ancestors", 27 | "dependencies": { 28 | "escape-string-regexp": "^4.0.0", 29 | "postcss-resolve-nested-selector": "^0.1.1" 30 | }, 31 | "devDependencies": { 32 | "ava": "^6.1.3", 33 | "nyc": "^17.0.0", 34 | "postcss": "^8.4.13", 35 | "prettier": "^3.3.1" 36 | }, 37 | "peerDependencies": { 38 | "postcss": "^8.0.0" 39 | }, 40 | "scripts": { 41 | "test": "nyc --reporter=lcov ava", 42 | "prepare": "npm run prettier:check && npm test", 43 | "version": "git add package.json", 44 | "postversion": "git push && git push --tags", 45 | "prettier:fix": "prettier --write .", 46 | "prettier:check": "prettier --check ." 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | const test = require('ava'); 3 | const plugin = require('./'); 4 | 5 | function run(t, input, output, opts = {}, warnings = 0) { 6 | return postcss([plugin(opts)]) 7 | .process(input) 8 | .then((result) => { 9 | t.deepEqual(result.css, output); 10 | t.deepEqual(result.warnings().length, warnings); 11 | }); 12 | } 13 | 14 | test('Chain ancestor in a simple case', (t) => { 15 | return run(t, '.a{ &:hover{ ^&-b{} } }', '.a{ &:hover{ .a-b{} } }', {}); 16 | }); 17 | 18 | test('Prepend ancestor in a simple case', (t) => { 19 | return run(t, '.a{ &:hover{ ^& .b{} } }', '.a{ &:hover{ .a .b{} } }', {}); 20 | }); 21 | 22 | test('Replace ancestor selectors in sibling rules', (t) => { 23 | return run( 24 | t, 25 | '.a{ &:hover{ ^&-b1{} ^&-b2{} ^& .b3{} } }', 26 | '.a{ &:hover{ .a-b1{} .a-b2{} .a .b3{} } }', 27 | {} 28 | ); 29 | }); 30 | 31 | test('Chain 2 ancestors in double selector', (t) => { 32 | return run( 33 | t, 34 | '.a{ &:hover{ ^&-b, ^&-c{} } }', 35 | '.a{ &:hover{ .a-b, .a-c{} } }', 36 | {} 37 | ); 38 | }); 39 | 40 | test('Prepend 2 ancestors in double selector', (t) => { 41 | return run( 42 | t, 43 | '.a{ &:hover{ ^& .b, ^& .c{} } }', 44 | '.a{ &:hover{ .a .b, .a .c{} } }', 45 | {} 46 | ); 47 | }); 48 | 49 | test('Return empty string when pointing to a non-existent ancestor', (t) => { 50 | return run( 51 | t, 52 | '.a{ &-b{ &-c{ ^^^^&-d{} } } }', 53 | '.a{ &-b{ &-c{ -d{} } } }', 54 | {}, 55 | 1 56 | ); 57 | }); 58 | 59 | test('Process 2 nested ancestor selectors', (t) => { 60 | return run( 61 | t, 62 | '.a{ &-b{ &-c{ ^^&-d{ &-e{ ^^^^&-f{ } } } } } }', 63 | '.a{ &-b{ &-c{ .a-d{ &-e{ .a-f{ } } } } } }', 64 | {} 65 | ); 66 | }); 67 | 68 | test('Replace with root comment', (t) => { 69 | return run( 70 | t, 71 | '/* This is a comment */ .a{ &:hover{ ^& .b, ^& .c{} } }', 72 | '/* This is a comment */ .a{ &:hover{ .a .b, .a .c{} } }', 73 | {} 74 | ); 75 | }); 76 | 77 | test('Replace with nested comment', (t) => { 78 | return run( 79 | t, 80 | '.a{ &:hover{ /* This is a comment */ ^& .b, ^& .c{} } }', 81 | '.a{ &:hover{ /* This is a comment */ .a .b, .a .c{} } }', 82 | {} 83 | ); 84 | }); 85 | 86 | // replaceDeclarations option 87 | test('Replace declaration values (I)', (t) => { 88 | return run( 89 | t, 90 | '.a{ &:hover { &:before { content: "^&"; } } }', 91 | '.a{ &:hover { &:before { content: ".a:hover"; } } }', 92 | { replaceDeclarations: true } 93 | ); 94 | }); 95 | 96 | test('Replace declaration values (II)', (t) => { 97 | return run( 98 | t, 99 | '.a{ &:hover { &:before { content: "^^&.foo"; } } }', 100 | '.a{ &:hover { &:before { content: ".a.foo"; } } }', 101 | { replaceDeclarations: true } 102 | ); 103 | }); 104 | 105 | test('Replace declaration values with multiple parent selector', (t) => { 106 | return run( 107 | t, 108 | '.a1,.a2{ &:hover { &:before { content: "^^&"; } } }', 109 | '.a1,.a2{ &:hover { &:before { content: ".a1,.a2"; } } }', 110 | { replaceDeclarations: true } 111 | ); 112 | }); 113 | 114 | test('Replace ancestors at different nesting levels', (t) => { 115 | return run( 116 | t, 117 | '.a{ &:hover{ ^&-b{} } .c{ .d{ ^&-e{} } } .z{} }', 118 | '.a{ &:hover{ .a-b{} } .c{ .d{ .a .c-e{} } } .z{} }', 119 | {} 120 | ); 121 | }); 122 | 123 | test('Replace ancestors with 4 different hierarchy levels (1 exceeding)', (t) => { 124 | return run( 125 | t, 126 | '.a{ &-b{ &-c{ &-d{} ^&-d,^&-d{} ^^&-d{} ^^^&-d{} } } }', 127 | '.a{ &-b{ &-c{ &-d{} .a-b-d,.a-b-d{} .a-d{} -d{} } } }', 128 | {}, 129 | 1 130 | ); 131 | }); 132 | 133 | test('Process nested ancestor close to > , + and ~ selectors', (t) => { 134 | return run( 135 | t, 136 | '.a{ &-b{ > ^&-c{} + ^&-d{} ~ ^&-e{} } }', 137 | '.a{ &-b{ > .a-c{} + .a-d{} ~ .a-e{} } }', 138 | {} 139 | ); 140 | }); 141 | 142 | test('Replace default ancestor selector with "£%"', (t) => { 143 | return run( 144 | t, 145 | '.a{ &-b{ &-c{ ££%-d{ £££%-f{ } } } } }', 146 | '.a{ &-b{ &-c{ .a-d{ .a-f{ } } } } }', 147 | { placeholder: '£%' } 148 | ); 149 | }); 150 | 151 | test('Replace default ancestor with custom levelSymbol and parentSymbol', (t) => { 152 | return run( 153 | t, 154 | '.a{ &-b{ &-c{ foofoobar-d{ foofoofoobar-f{ } } } } }', 155 | '.a{ &-b{ &-c{ .a-d{ .a-f{ } } } } }', 156 | { levelSymbol: 'foo', parentSymbol: 'bar' } 157 | ); 158 | }); 159 | 160 | // Complex nesting 161 | test('Generate comma separated selectors when root element has multiple selectors', (t) => { 162 | return run( 163 | t, 164 | '.a1,.a2{ .b{ &:hover{ ^& .c{} } } }', 165 | '.a1,.a2{ .b{ &:hover{ .a1 .b .c,.a2 .b .c{} } } }', 166 | {} 167 | ); 168 | }); 169 | 170 | test('Generate comma separated selectors when root element has multiple selectors + "&" selector', (t) => { 171 | return run( 172 | t, 173 | '.a1,.a2{ &-b{ &:hover{ ^&-c{} } } }', 174 | '.a1,.a2{ &-b{ &:hover{ .a1-b-c,.a2-b-c{} } } }', 175 | {} 176 | ); 177 | }); 178 | 179 | test('Generate comma separated selectors when two ancestors have multiple selectors', (t) => { 180 | return run( 181 | t, 182 | '.a1,.a2{ .b1,.b2{ &:hover{ ^& .c{} } } }', 183 | '.a1,.a2{ .b1,.b2{ &:hover{ .a1 .b1 .c,.a2 .b1 .c,.a1 .b2 .c,.a2 .b2 .c{} } } }', 184 | {} 185 | ); 186 | }); 187 | 188 | test('Generate comma separated selectors when two ancestors have multiple selectors + "&" selectors', (t) => { 189 | return run( 190 | t, 191 | '.a1,.a2{ &-b1,&-b2{ &:hover{ ^&-c{} } } }', 192 | '.a1,.a2{ &-b1,&-b2{ &:hover{ .a1-b1-c,.a2-b1-c,.a1-b2-c,.a2-b2-c{} } } }', 193 | {} 194 | ); 195 | }); 196 | 197 | test('Generate comma separated selectors when two ancestors have multiple selectors + one "&" selector', (t) => { 198 | return run( 199 | t, 200 | '.a1,.a2{ .b1,&-b2{ &:hover{ ^&-c{} } } }', 201 | '.a1,.a2{ .b1,&-b2{ &:hover{ .a1 .b1-c,.a2 .b1-c,.a1-b2-c,.a2-b2-c{} } } }', 202 | {} 203 | ); 204 | }); 205 | 206 | test('Do not consider media queries when building ancestor selectors', (t) => { 207 | return run( 208 | t, 209 | '.a{ &-b{ &-c{ @media (max-width: 320px){ ^&-d{} &-d{} } } } }', 210 | '.a{ &-b{ &-c{ @media (max-width: 320px){ .a-b-d{} &-d{} } } } }', 211 | {} 212 | ); 213 | }); 214 | 215 | // Ancestor selector repeated in same rule 216 | test('Use same ancestor selector twice in same rule', (t) => { 217 | return run( 218 | t, 219 | '.a{ &:hover{ ^&^& .b{} ^&^&-b{} } }', 220 | '.a{ &:hover{ .a.a .b{} .a.a-b{} } }', 221 | {}, 222 | 2 223 | ); 224 | }); 225 | 226 | test('Use same ancestor selector twice in same rule inside multiple selector rule (I)', (t) => { 227 | return run( 228 | t, 229 | '.a1,.a2{ &:hover{ ^&^&-b{} } }', 230 | '.a1,.a2{ &:hover{ .a1.a1-b,.a2.a2-b{} } }', 231 | {}, 232 | 1 233 | ); 234 | }); 235 | 236 | test('Use same ancestor selector twice in same rule inside multiple selector rule (II)', (t) => { 237 | return run( 238 | t, 239 | '.a1,.a2{ &-b1,&-b2{ &:hover{ ^&^&-c{} } } }', 240 | '.a1,.a2{ &-b1,&-b2{ &:hover{ .a1-b1.a1-b1-c,.a2-b1.a2-b1-c,.a1-b2.a1-b2-c,.a2-b2.a2-b2-c{} } } }', 241 | {}, 242 | 1 243 | ); 244 | }); 245 | 246 | test.failing('Use two different ancestor placeholders in same rule', (t) => { 247 | return run( 248 | t, 249 | '.a{ &-b{ &:hover{ ^&^^&-c{} } } }', 250 | '.a{ &-b{ &:hover{ .a-b.a-c{} } } }', 251 | {}, 252 | 1 253 | ); 254 | }); 255 | 256 | test('Alternative to having two different ancestor placeholders in same rule', (t) => { 257 | return run( 258 | t, 259 | '.a{ &-b{ &:hover{ ^& { &^^^&-c{} } } } }', 260 | '.a{ &-b{ &:hover{ .a-b { &.a-c{} } } } }', 261 | {} 262 | ); 263 | }); 264 | --------------------------------------------------------------------------------