├── test ├── injected │ ├── style.css │ └── extend.css ├── injected-extend.css ├── injected-styles.css ├── injected-styles.expect.css ├── issue-10.expect.css ├── issue-8.css ├── injected-extend.expect.css ├── issue-8.expect.css ├── spec-example-7.expect.css ├── issue-10.css ├── spec-example-1.css ├── spec-example-7.css ├── basic-button.css ├── nested-media.css ├── basic.css ├── basic.name.expect.css ├── spec-example-3.css ├── spec-example-6.css ├── spec-example-1.expect.css ├── basic-postcss-name.css ├── spec-example-5.css ├── basic-button.expect.css ├── spec-example-4.expect.css ├── spec-example-6.expect.css ├── spec-example-4.css ├── nested-media.expect.css ├── basic.expect.css ├── spec-example-3.expect.css ├── basic-postcss-name.expect.css ├── nested-media.nesting-first.expect.css ├── nested-media.nesting-second.expect.css ├── error.expect.css ├── advanced.expect.css ├── advanced.css ├── spec-example-5.expect.css ├── error.css ├── error.ignore.expect.css ├── error.warn.expect.css └── _tape.mjs ├── .gitignore ├── .editorconfig ├── rollup.mjs ├── .github └── workflows │ └── test.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── package.json ├── CODE_OF_CONDUCT.md ├── README.md ├── src └── index.js ├── dist ├── index.cjs └── index.mjs └── LICENSE.md /test/injected/style.css: -------------------------------------------------------------------------------- 1 | .button { 2 | color: green; 3 | } 4 | -------------------------------------------------------------------------------- /test/injected-extend.css: -------------------------------------------------------------------------------- 1 | .your-button { 2 | color: green; 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eslintcache 2 | *.log* 3 | *.result.css 4 | node_modules 5 | -------------------------------------------------------------------------------- /test/injected-styles.css: -------------------------------------------------------------------------------- 1 | .my-button { 2 | @extend .button; 3 | } 4 | -------------------------------------------------------------------------------- /test/injected-styles.expect.css: -------------------------------------------------------------------------------- 1 | .my-button { 2 | color: green; 3 | } 4 | -------------------------------------------------------------------------------- /test/injected/extend.css: -------------------------------------------------------------------------------- 1 | .button { 2 | @extend .your-button; 3 | } 4 | -------------------------------------------------------------------------------- /test/issue-10.expect.css: -------------------------------------------------------------------------------- 1 | .baz { 2 | right: 0; 3 | } 4 | 5 | .baz { 6 | @extend %bar; 7 | } 8 | -------------------------------------------------------------------------------- /test/issue-8.css: -------------------------------------------------------------------------------- 1 | .test { 2 | display: none; 3 | } 4 | 5 | .thing:before { 6 | @extend .test; 7 | } 8 | -------------------------------------------------------------------------------- /test/injected-extend.expect.css: -------------------------------------------------------------------------------- 1 | .your-button { 2 | color: green; 3 | }.button { 4 | color: green; 5 | } 6 | -------------------------------------------------------------------------------- /test/issue-8.expect.css: -------------------------------------------------------------------------------- 1 | .test { 2 | display: none; 3 | } 4 | 5 | .thing:before { 6 | display: none; 7 | } 8 | -------------------------------------------------------------------------------- /test/spec-example-7.expect.css: -------------------------------------------------------------------------------- 1 | .image-post { 2 | overflow: auto; 3 | } 4 | 5 | .image-post>img { 6 | float: left; 7 | } 8 | -------------------------------------------------------------------------------- /test/issue-10.css: -------------------------------------------------------------------------------- 1 | %foo { 2 | right: 0; 3 | } 4 | 5 | %bar { 6 | left: 0; 7 | } 8 | 9 | .baz { 10 | @extend %foo; 11 | @extend %bar; 12 | } 13 | -------------------------------------------------------------------------------- /test/spec-example-1.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: red; 3 | border: thick dotted red; 4 | } 5 | 6 | .serious-error { 7 | @extend .error; 8 | font-weight: bold; 9 | } 10 | -------------------------------------------------------------------------------- /test/spec-example-7.css: -------------------------------------------------------------------------------- 1 | %media-block { 2 | overflow: auto; 3 | } 4 | 5 | %media-block>img { 6 | float: left; 7 | } 8 | 9 | .image-post { 10 | @extend %media-block; 11 | } 12 | -------------------------------------------------------------------------------- /test/basic-button.css: -------------------------------------------------------------------------------- 1 | button { 2 | color: red; 3 | } 4 | 5 | .button { 6 | @extend button; 7 | background: blue; 8 | } 9 | 10 | #button { 11 | @extend button; 12 | background: lime; 13 | } 14 | -------------------------------------------------------------------------------- /test/nested-media.css: -------------------------------------------------------------------------------- 1 | .my_placeholder { 2 | color: red; 3 | 4 | @media screen and (min-width: 920px) { 5 | color: green; 6 | } 7 | } 8 | 9 | .test-content { 10 | @extend .my_placeholder; 11 | } 12 | -------------------------------------------------------------------------------- /test/basic.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | border: thick dotted red; 3 | color: red; 4 | } 5 | 6 | .modal:hover { 7 | outline: none; 8 | } 9 | 10 | .serious-modal { 11 | @extend .modal; 12 | 13 | font-weight: bold; 14 | } 15 | -------------------------------------------------------------------------------- /test/basic.name.expect.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | border: thick dotted red; 3 | color: red; 4 | } 5 | 6 | .modal:hover { 7 | outline: none; 8 | } 9 | 10 | .serious-modal { 11 | @extend .modal; 12 | 13 | font-weight: bold; 14 | } 15 | -------------------------------------------------------------------------------- /test/spec-example-3.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: red; 3 | } 4 | 5 | @media (width > 600px) { 6 | .serious-error { 7 | @extend .error; 8 | 9 | font-weight: bold; 10 | } 11 | 12 | .error { 13 | width: 100%; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/spec-example-6.css: -------------------------------------------------------------------------------- 1 | .media-block { 2 | overflow: auto; 3 | } 4 | 5 | .media-block > img { 6 | float: left; 7 | } 8 | 9 | .image-post { 10 | @extend .media-block; 11 | /* additional styles to tweak the display */ 12 | } 13 | -------------------------------------------------------------------------------- /test/spec-example-1.expect.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: red; 3 | border: thick dotted red; 4 | } 5 | 6 | .serious-error { 7 | color: red; 8 | border: thick dotted red; 9 | } 10 | 11 | .serious-error { 12 | font-weight: bold; 13 | } 14 | -------------------------------------------------------------------------------- /test/basic-postcss-name.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | border: thick dotted red; 3 | color: red; 4 | } 5 | 6 | .modal:hover { 7 | outline: none; 8 | } 9 | 10 | .serious-modal { 11 | @postcss-extend .modal; 12 | 13 | font-weight: bold; 14 | } 15 | -------------------------------------------------------------------------------- /test/spec-example-5.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: red; 3 | } 4 | 5 | .serious-error { 6 | @extend .error; 7 | 8 | font-weight: bold; 9 | } 10 | 11 | .super-serious-error { 12 | @extend .serious-error; 13 | 14 | animation: flashing 1s infinite; 15 | } 16 | -------------------------------------------------------------------------------- /test/basic-button.expect.css: -------------------------------------------------------------------------------- 1 | button { 2 | color: red; 3 | } 4 | 5 | .button { 6 | color: red; 7 | } 8 | 9 | .button { 10 | background: blue; 11 | } 12 | 13 | #button { 14 | color: red; 15 | } 16 | 17 | #button { 18 | background: lime; 19 | } 20 | -------------------------------------------------------------------------------- /test/spec-example-4.expect.css: -------------------------------------------------------------------------------- 1 | button { 2 | color: green; 3 | } 4 | 5 | .my-button { 6 | color: green; 7 | } 8 | 9 | .other-button { 10 | color: orange; 11 | } 12 | 13 | .other-button:active { 14 | color: purple; 15 | } 16 | 17 | .perma-pressed-button { 18 | color: purple; 19 | } 20 | -------------------------------------------------------------------------------- /test/spec-example-6.expect.css: -------------------------------------------------------------------------------- 1 | .media-block { 2 | overflow: auto; 3 | } 4 | 5 | .media-block > img { 6 | float: left; 7 | } 8 | 9 | .image-post { 10 | overflow: auto; 11 | } 12 | 13 | .image-post > img { 14 | float: left; 15 | } 16 | 17 | /* additional styles to tweak the display */ 18 | -------------------------------------------------------------------------------- /test/spec-example-4.css: -------------------------------------------------------------------------------- 1 | button { 2 | color: green; 3 | } 4 | 5 | .my-button { 6 | @extend button; 7 | } 8 | 9 | .other-button { 10 | color: orange; 11 | } 12 | 13 | .other-button:active { 14 | color: purple; 15 | } 16 | 17 | .perma-pressed-button { 18 | @extend .other-button:active; 19 | } 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = tab 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | 13 | [*.{json,md,yml}] 14 | indent_size = 2 15 | indent_style = space 16 | -------------------------------------------------------------------------------- /test/nested-media.expect.css: -------------------------------------------------------------------------------- 1 | .my_placeholder { 2 | color: red; 3 | 4 | @media screen and (min-width: 920px) { 5 | color: green; 6 | } 7 | } 8 | 9 | .test-content { 10 | color: red; 11 | } 12 | 13 | @media screen and (min-width: 920px) { 14 | 15 | .test-content { 16 | color: green; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/basic.expect.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | border: thick dotted red; 3 | color: red; 4 | } 5 | 6 | .modal:hover { 7 | outline: none; 8 | } 9 | 10 | .serious-modal { 11 | border: thick dotted red; 12 | color: red; 13 | } 14 | 15 | .serious-modal:hover { 16 | outline: none; 17 | } 18 | 19 | .serious-modal { 20 | 21 | font-weight: bold; 22 | } 23 | -------------------------------------------------------------------------------- /test/spec-example-3.expect.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: red; 3 | } 4 | 5 | @media (width > 600px) { 6 | .serious-error { 7 | color: red; 8 | } 9 | 10 | @media (width > 600px) { 11 | .serious-error { 12 | width: 100%; 13 | } 14 | } 15 | .serious-error { 16 | 17 | font-weight: bold; 18 | } 19 | 20 | .error { 21 | width: 100%; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/basic-postcss-name.expect.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | border: thick dotted red; 3 | color: red; 4 | } 5 | 6 | .modal:hover { 7 | outline: none; 8 | } 9 | 10 | .serious-modal { 11 | border: thick dotted red; 12 | color: red; 13 | } 14 | 15 | .serious-modal:hover { 16 | outline: none; 17 | } 18 | 19 | .serious-modal { 20 | 21 | font-weight: bold; 22 | } 23 | -------------------------------------------------------------------------------- /test/nested-media.nesting-first.expect.css: -------------------------------------------------------------------------------- 1 | .my_placeholder { 2 | color: red; 3 | } 4 | 5 | @media screen and (min-width: 920px) { 6 | 7 | .my_placeholder { 8 | color: green; 9 | } 10 | } 11 | 12 | .test-content { 13 | color: red; 14 | } 15 | 16 | @media screen and (min-width: 920px) { 17 | 18 | .test-content { 19 | color: green; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/nested-media.nesting-second.expect.css: -------------------------------------------------------------------------------- 1 | .my_placeholder { 2 | color: red; 3 | } 4 | 5 | @media screen and (min-width: 920px) { 6 | 7 | .my_placeholder { 8 | color: green; 9 | } 10 | } 11 | 12 | .test-content { 13 | color: red; 14 | } 15 | 16 | @media screen and (min-width: 920px) { 17 | 18 | .test-content { 19 | color: green; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/error.expect.css: -------------------------------------------------------------------------------- 1 | test-does-not-extend-non-existent-selector { 2 | } 3 | 4 | test-does-not-extend-itself { 5 | @extend test-does-not-extend-itself; 6 | } 7 | 8 | test-does-not-extend-itself-cleverly-1 { 9 | @extend test-does-not-extend-itself-cleverly-1; 10 | } 11 | 12 | test-does-not-extend-itself-cleverly-2 { 13 | @extend test-does-not-extend-itself-cleverly-1; 14 | } 15 | -------------------------------------------------------------------------------- /test/advanced.expect.css: -------------------------------------------------------------------------------- 1 | .serious-modal { 2 | font-style: normal; 3 | font-weight: bold; 4 | } 5 | 6 | @media (max-width: 240px) { 7 | 8 | .serious-modal:not(:focus) { 9 | outline: none; 10 | } 11 | } 12 | 13 | .modal { 14 | border: thick dotted red; 15 | } 16 | 17 | .modal { 18 | 19 | color: red; 20 | } 21 | 22 | .modal:hover:not(:focus) { 23 | outline: none; 24 | } 25 | -------------------------------------------------------------------------------- /test/advanced.css: -------------------------------------------------------------------------------- 1 | %thick-border { 2 | border: thick dotted red; 3 | } 4 | 5 | .serious-modal { 6 | font-style: normal; 7 | font-weight: bold; 8 | 9 | @media (max-width: 240px) { 10 | @extend .modal:hover; 11 | } 12 | } 13 | 14 | .modal { 15 | @extend %thick-border; 16 | 17 | color: red; 18 | 19 | &:hover { 20 | &:not(:focus) { 21 | outline: none; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/spec-example-5.expect.css: -------------------------------------------------------------------------------- 1 | .error { 2 | color: red; 3 | } 4 | 5 | .serious-error { 6 | color: red; 7 | } 8 | 9 | .serious-error { 10 | 11 | font-weight: bold; 12 | } 13 | 14 | .super-serious-error { 15 | color: red; 16 | } 17 | 18 | .super-serious-error { 19 | 20 | font-weight: bold; 21 | } 22 | 23 | .super-serious-error { 24 | 25 | animation: flashing 1s infinite; 26 | } 27 | -------------------------------------------------------------------------------- /test/error.css: -------------------------------------------------------------------------------- 1 | test-does-not-extend-non-existent-selector { 2 | @extend some-non-existent-selector; 3 | } 4 | 5 | test-does-not-extend-itself { 6 | @extend test-does-not-extend-itself; 7 | } 8 | 9 | test-does-not-extend-itself-cleverly-1 { 10 | @extend test-does-not-extend-itself-cleverly-2; 11 | } 12 | 13 | test-does-not-extend-itself-cleverly-2 { 14 | @extend test-does-not-extend-itself-cleverly-1; 15 | } 16 | 17 | %test-placeholder { 18 | @extend %test-placeholder; 19 | } 20 | -------------------------------------------------------------------------------- /test/error.ignore.expect.css: -------------------------------------------------------------------------------- 1 | test-does-not-extend-non-existent-selector { 2 | @extend some-non-existent-selector; 3 | } 4 | 5 | test-does-not-extend-itself { 6 | @extend test-does-not-extend-itself; 7 | } 8 | 9 | test-does-not-extend-itself-cleverly-1 { 10 | @extend test-does-not-extend-itself-cleverly-1; 11 | } 12 | 13 | test-does-not-extend-itself-cleverly-2 { 14 | @extend test-does-not-extend-itself-cleverly-1; 15 | } 16 | 17 | %test-placeholder { 18 | @extend %test-placeholder; 19 | } 20 | -------------------------------------------------------------------------------- /test/error.warn.expect.css: -------------------------------------------------------------------------------- 1 | test-does-not-extend-non-existent-selector { 2 | @extend some-non-existent-selector; 3 | } 4 | 5 | test-does-not-extend-itself { 6 | @extend test-does-not-extend-itself; 7 | } 8 | 9 | test-does-not-extend-itself-cleverly-1 { 10 | @extend test-does-not-extend-itself-cleverly-1; 11 | } 12 | 13 | test-does-not-extend-itself-cleverly-2 { 14 | @extend test-does-not-extend-itself-cleverly-1; 15 | } 16 | 17 | %test-placeholder { 18 | @extend %test-placeholder; 19 | } 20 | -------------------------------------------------------------------------------- /rollup.mjs: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | 3 | export default { 4 | input: 'src/index.js', 5 | output: [ 6 | { file: 'dist/index.cjs', format: 'cjs', sourcemap: false, strict: false, exports: 'auto' }, 7 | { file: 'dist/index.mjs', format: 'esm', sourcemap: false, strict: false, exports: 'auto' }, 8 | ], 9 | plugins: [ 10 | babel({ 11 | babelHelpers: 'bundled', 12 | presets: [ 13 | ['@babel/env', { modules: false, targets: { node: 12 } }], 14 | ], 15 | }), 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | 5 | concurrency: 6 | group: branch-node-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node: [16, 18, 'lts/*'] 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node }} 20 | 21 | - run: npm i 22 | - run: npm run build 23 | - run: npm run test 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes to PostCSS Extend Rule 2 | 3 | ### 4.0.0 (February 10, 2022) 4 | 5 | - PostCSS 8 support (thanks to [@bjentsch](https://github.com/bjentsch)!) (breaking) 6 | - Supporting only Node 12, 14 and >= 16 (breaking). 7 | - Updated `postcss-nesting` to `7.0.1` (major) 8 | - Updated `babel-core`, `@babel/preset-env` to `7.5.5` (major) 9 | - Updated `eslint` to `6.1.0` (major) 10 | - Updated `rollup` to `1.17.0` (major) 11 | - Updated `rollup-plugin-babel` to `4.3.3` (major) 12 | 13 | ### 3.0.0 (July 29, 2019) 14 | 15 | ### 2.0.0 (September 19, 2017) 16 | 17 | - Added: `name` option to override the name of the extending at-rule. 18 | - Updated: `postcss-nesting` to v5.0.0 (major) 19 | - Updated: How the project is developed 20 | 21 | ### 1.1.0 (September 19, 2017) 22 | 23 | - Improve: Un-nesting of extended elements 24 | 25 | ### 1.0.0 (September 15, 2017) 26 | 27 | - Initial version 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to PostCSS Extend Rule 2 | 3 | You want to help? You rock! Now, take a moment to be sure your contributions 4 | make sense to everyone else. 5 | 6 | ## Reporting Issues 7 | 8 | Found a problem? Want a new feature? 9 | 10 | - See if your issue or idea has [already been reported]. 11 | - Provide a [reduced test case] or a [live example]. 12 | 13 | Remember, a bug is a _demonstrable problem_ caused by _our_ code. 14 | 15 | ## Submitting Pull Requests 16 | 17 | Pull requests are the greatest contributions, so be sure they are focused in 18 | scope and avoid unrelated commits. 19 | 20 | 1. To begin; [fork this project], clone your fork, and add our upstream. 21 | ```bash 22 | # Clone your fork of the repo into the current directory 23 | git clone git@github.com:YOUR_USER/postcss-extend-rule.git 24 | 25 | # Navigate to the newly cloned directory 26 | cd postcss-extend-rule 27 | 28 | # Assign the original repo to a remote called "upstream" 29 | git remote add upstream git@github.com:csstools/postcss-extend-rule.git 30 | 31 | # Install the tools necessary for testing 32 | npm install 33 | ``` 34 | 35 | 2. Create a branch for your feature or fix: 36 | ```bash 37 | # Move into a new branch for your feature 38 | git checkout -b feature/thing 39 | ``` 40 | ```bash 41 | # Move into a new branch for your fix 42 | git checkout -b fix/something 43 | ``` 44 | 45 | 3. If your code follows our practices, then push your feature branch: 46 | ```bash 47 | # Test current code 48 | npm test 49 | ``` 50 | ```bash 51 | # Push the branch for your new feature 52 | git push origin feature/thing 53 | ``` 54 | ```bash 55 | # Or, push the branch for your update 56 | git push origin update/something 57 | ``` 58 | 59 | That’s it! Now [open a pull request] with a clear title and description. 60 | 61 | [already been reported]: issues 62 | [fork this project]: fork 63 | [live example]: https://codepen.io/pen 64 | [open a pull request]: https://help.github.com/articles/using-pull-requests/ 65 | [reduced test case]: https://css-tricks.com/reduced-test-cases/ 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-extend-rule", 3 | "version": "4.0.0", 4 | "description": "Use the @extend at-rule and functional selectors in CSS", 5 | "author": "Jonathan Neal ", 6 | "license": "CC0-1.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/csstools/postcss-extend-rule.git" 10 | }, 11 | "homepage": "https://github.com/csstools/postcss-extend-rule#readme", 12 | "bugs": "https://github.com/csstools/postcss-extend-rule/issues", 13 | "main": "dist/index.cjs", 14 | "module": "dist/index.mjs", 15 | "exports": { 16 | ".": { 17 | "import": "./dist/index.mjs", 18 | "require": "./dist/index.cjs", 19 | "default": "./dist/index.mjs" 20 | } 21 | }, 22 | "files": [ 23 | "CHANGELOG.md", 24 | "LICENSE.md", 25 | "README.md", 26 | "dist" 27 | ], 28 | "scripts": { 29 | "build": "rollup --config rollup.mjs --silent", 30 | "clean": "node -e \"fs.rmSync('./dist', { recursive: true, force: true });\"", 31 | "prepublishOnly": "npm run clean && npm run build && npm run test", 32 | "pretest": "npm run build", 33 | "test": "npm run lint && node --test", 34 | "test:rewrite-expects": "REWRITE_EXPECTS=true node --test", 35 | "lint": "eslint src/{*,**/*}.js test/*.mjs --cache --ignore-path .gitignore --quiet" 36 | }, 37 | "engines": { 38 | "node": "^12 || ^14 || >=16" 39 | }, 40 | "dependencies": { 41 | "postcss-nesting": "^10.1.2" 42 | }, 43 | "devDependencies": { 44 | "@babel/core": "^7.17.2", 45 | "@babel/eslint-parser": "^7.17.0", 46 | "@babel/preset-env": "^7.16.11", 47 | "@csstools/postcss-global-data": "^2.1.1", 48 | "@csstools/postcss-tape": "^4.1.1", 49 | "@rollup/plugin-babel": "^6.0.4", 50 | "eslint": "^8.8.0", 51 | "postcss": "^8.4.6", 52 | "rollup": "^4.9.1" 53 | }, 54 | "peerDependencies": { 55 | "postcss": "^8.4.6" 56 | }, 57 | "postcssConfig": { 58 | "config": ".tape.js" 59 | }, 60 | "eslintConfig": { 61 | "env": { 62 | "es6": true, 63 | "node": true 64 | }, 65 | "extends": "eslint:recommended", 66 | "rules": { 67 | "quotes": [ 68 | "error", 69 | "single" 70 | ], 71 | "comma-dangle": [ 72 | "error", 73 | "always-multiline" 74 | ], 75 | "semi": [ 76 | "error", 77 | "always" 78 | ], 79 | "curly": "error", 80 | "brace-style": "error", 81 | "indent": [ 82 | "error", 83 | "tab", 84 | { 85 | "SwitchCase": 1 86 | } 87 | ], 88 | "radix": "error" 89 | }, 90 | "parserOptions": { 91 | "ecmaVersion": 2020, 92 | "sourceType": "module" 93 | }, 94 | "root": true 95 | }, 96 | "keywords": [ 97 | "postcss", 98 | "css", 99 | "postcss-plugin", 100 | "extend", 101 | "matched", 102 | "matches", 103 | "match", 104 | "selectors", 105 | "subclassing", 106 | "subclasses", 107 | "subclass", 108 | "styling", 109 | "styles", 110 | "style", 111 | "placeholder", 112 | "placehold", 113 | "selectors", 114 | "selector", 115 | "chaining" 116 | ] 117 | } 118 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting Jonathan Neal . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /test/_tape.mjs: -------------------------------------------------------------------------------- 1 | import { postcssTape } from '@csstools/postcss-tape'; 2 | import plugin from 'postcss-extend-rule'; 3 | import postcssNesting from 'postcss-nesting'; 4 | import postcssGlobalData from '@csstools/postcss-global-data'; 5 | 6 | postcssTape(plugin)({ 7 | 'basic': { 8 | message: 'supports @extend usage', 9 | }, 10 | 'basic:name': { 11 | message: 'ignores @extend usage when { name: "postcss-extend" }', 12 | options: { 13 | name: 'postcss-extend', 14 | }, 15 | }, 16 | 'basic-postcss-name': { 17 | message: 'supports @postcss-extend when { name: "postcss-extend" }', 18 | options: { 19 | name: 'postcss-extend', 20 | }, 21 | }, 22 | 'basic-button': { 23 | message: 'supports @extend usage with same tag name and class name', 24 | }, 25 | 'injected-extend': { 26 | message: 'supports injected extend usage', 27 | plugins: [ 28 | plugin, 29 | postcssGlobalData({ 30 | files: [ 31 | 'test/injected/extend.css', 32 | ], 33 | }), 34 | ], 35 | }, 36 | 'injected-styles': { 37 | message: 'supports injected styles usage', 38 | plugins: [ 39 | plugin, 40 | postcssGlobalData({ 41 | files: [ 42 | 'test/injected/style.css', 43 | ], 44 | }), 45 | ], 46 | }, 47 | 'advanced': { 48 | message: 'supports mixed usage (with postcss-nesting)', 49 | plugins: [postcssNesting, plugin], 50 | }, 51 | 'nested-media': { 52 | 'message': 'supports nested @media usage', 53 | }, 54 | 'nested-media:nesting-first': { 55 | 'message': 'supports nested @media usage when postcss-nesting runs first', 56 | plugins: [postcssNesting, plugin], 57 | }, 58 | 'nested-media:nesting-second': { 59 | 'message': 'supports nested @media usage when postcss-nesting runs second', 60 | plugins: [plugin, postcssNesting], 61 | }, 62 | 'error': { 63 | message: 'manages error-ridden usage', 64 | }, 65 | 'error:ignore': { 66 | message: 'manages error-ridden usage with { onFunctionalSelector: "ignore", onRecursiveExtend: "ignore", onUnusedExtend: "ignore" } options', 67 | options: { 68 | onFunctionalSelector: 'ignore', 69 | onRecursiveExtend: 'ignore', 70 | onUnusedExtend: 'ignore', 71 | }, 72 | }, 73 | 'error:warn': { 74 | message: 'manages error-ridden usage with { onFunctionalSelector: "warn", onRecursiveExtend: "warn", onUnusedExtend: "warn" } options', 75 | options: { 76 | onFunctionalSelector: 'warn', 77 | onRecursiveExtend: 'warn', 78 | onUnusedExtend: 'warn', 79 | }, 80 | warnings: 2, 81 | }, 82 | 'error:throw': { 83 | message: 'manages error-ridden usage with { onFunctionalSelector: "throw", onRecursiveExtend: "throw", onUnusedExtend: "throw" } options', 84 | options: { 85 | onFunctionalSelector: 'throw', 86 | onRecursiveExtend: 'throw', 87 | onUnusedExtend: 'throw', 88 | }, 89 | exception: /Unused extend at-rule "some-non-existent-selector"/, 90 | }, 91 | 'error:throw-on-functional-selectors': { 92 | message: 'manages error-ridden usage with { onFunctionalSelector: "throw" } options', 93 | options: { 94 | onFunctionalSelector: 'throw', 95 | }, 96 | exception: /Encountered functional selector "%test-placeholder"/, 97 | }, 98 | 'issue-8': { 99 | message: 'https://github.com/csstools/postcss-extend-rule/issues/8', 100 | }, 101 | 'issue-10': { 102 | message: 'https://github.com/csstools/postcss-extend-rule/issues/10', 103 | }, 104 | 'spec-example-1': { 105 | message: 'specification example 1', 106 | }, 107 | 'spec-example-3': { 108 | message: 'specification example 3', 109 | }, 110 | 'spec-example-4': { 111 | message: 'specification example 4', 112 | }, 113 | 'spec-example-5': { 114 | message: 'specification example 5', 115 | }, 116 | 'spec-example-6': { 117 | message: 'specification example 6', 118 | }, 119 | 'spec-example-7': { 120 | message: 'specification example 7', 121 | }, 122 | }); 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostCSS Extend Rule [PostCSS][postcss] 2 | 3 | [![NPM Version][npm-img]][npm-url] 4 | [![test](https://github.com/csstools/postcss-extend-rule/actions/workflows/test.yml/badge.svg)](https://github.com/csstools/postcss-extend-rule/actions/workflows/test.yml) 5 | [Discord][discord] 6 | 7 | [PostCSS Extend Rule] lets you use the `@extend` at-rule and 8 | [Functional Selectors] in CSS, following the speculative 9 | [CSS Extend Rules Specification]. 10 | 11 | ```pcss 12 | %thick-border { 13 | border: thick dotted red; 14 | } 15 | 16 | .serious-modal { 17 | font-style: normal; 18 | font-weight: bold; 19 | 20 | @media (max-width: 240px) { 21 | @extend .modal:hover; 22 | } 23 | } 24 | 25 | .modal { 26 | @extend %thick-border; 27 | 28 | color: red; 29 | } 30 | 31 | .modal:hover:not(:focus) { 32 | outline: none; 33 | } 34 | 35 | /* becomes */ 36 | 37 | .serious-modal { 38 | font-style: normal; 39 | font-weight: bold; 40 | } 41 | 42 | @media (max-width: 240px) { 43 | .serious-modal:not(:focus) { 44 | outline: none; 45 | } 46 | } 47 | 48 | .modal { 49 | border: thick dotted red; 50 | color: red; 51 | } 52 | 53 | .modal:hover:not(:focus) { 54 | outline: none; 55 | } 56 | ``` 57 | 58 | ## Usage 59 | 60 | Add [PostCSS Extend Rule] to your project: 61 | 62 | ```bash 63 | npm install postcss postcss-extend-rule --save-dev 64 | ``` 65 | 66 | Use **PostCSS Extend Rule** to process your CSS: 67 | 68 | ```js 69 | const postcssExtendRule = require('postcss-extend-rule'); 70 | 71 | postcssExtendRule.process(YOUR_CSS /*, processOptions, pluginOptions */); 72 | ``` 73 | 74 | Or use it as a [PostCSS] plugin: 75 | 76 | ```js 77 | const postcss = require('postcss'); 78 | const postcssExtendRule = require('postcss-extend-rule'); 79 | 80 | postcss([ 81 | postcssExtendRule(/* pluginOptions */) 82 | ]).process(YOUR_CSS /*, processOptions */); 83 | ``` 84 | 85 | **PostCSS Extend Rule** runs in all Node environments, with special instructions for: 86 | 87 | | [Node](INSTALL.md#node) | [PostCSS CLI](INSTALL.md#postcss-cli) | [Webpack](INSTALL.md#webpack) | [Create React App](INSTALL.md#create-react-app) | [Gulp](INSTALL.md#gulp) | [Grunt](INSTALL.md#grunt) | 88 | | --- | --- | --- | --- | --- | --- | 89 | 90 | ## Options 91 | 92 | ### name 93 | 94 | The `name` option determines the at-rule name being used to extend selectors. 95 | By default, this name is `extend`, meaning `@extend` rules are parsed. 96 | 97 | ```js 98 | postcssExtend({ name: 'postcss-extend' }) 99 | ``` 100 | 101 | If the `name` option were changed to, say, `postcss-extend`, then only 102 | `@postcss-extend` at-rules would be parsed. 103 | 104 | ```pcss 105 | main { 106 | @postcss-extend .some-rule; 107 | } 108 | ``` 109 | 110 | ### onFunctionalSelector 111 | 112 | The `onFunctionalSelector` option determines how functional selectors should be 113 | handled. Its options are: 114 | 115 | - `remove` (default) removes any functional selector 116 | - `ignore` ignores any functional selector and moves on 117 | - `warn` warns the user whenever it encounters a functional selector 118 | - `throw` throws an error if ever it encounters a functional selector 119 | 120 | ```js 121 | postcssExtend({ onFunctionalSelector: 'remove' /* default */ }) 122 | ``` 123 | 124 | ```pcss 125 | %this-will-be-removed {} 126 | ``` 127 | 128 | ### onRecursiveExtend 129 | 130 | The `onRecursiveExtend` option determines how recursive extend at-rules should 131 | be handled. Its options are: 132 | 133 | - `remove` (default) removes any recursive extend at-rules 134 | - `ignore` ignores any recursive extend at-rules and moves on 135 | - `warn` warns the user whenever it encounters a recursive extend at-rules 136 | - `throw` throws an error if ever it encounters a recursive extend at-rules 137 | 138 | ```js 139 | postcssExtend({ onRecursiveExtend: 'remove' /* default */ }) 140 | ``` 141 | 142 | ```pcss 143 | .this-will-not-extend-itself { 144 | @extend .this-will-not-extend-itself; 145 | } 146 | ``` 147 | 148 | ### onUnusedExtend 149 | 150 | The `onUnusedExtend` option determines how an unused extend at-rule should be 151 | handled. Its options are: 152 | 153 | - `remove` (default) removes any unused extend at-rule 154 | - `ignore` ignores any unused extend at-rule and moves on 155 | - `warn` warns the user whenever it encounters an unused extend at-rule 156 | - `throw` throws an error if ever it encounters an unused extend at-rule 157 | 158 | ```js 159 | postcssExtend({ onUnusedExtend: 'remove' /* default */ }) 160 | ``` 161 | 162 | ```pcss 163 | main { 164 | @extend .this-selector-does-not-exist-and-will-be-removed; 165 | } 166 | ``` 167 | 168 | [git-img]: https://img.shields.io/badge/support-chat-blue.svg 169 | [discord]: https://discord.gg/bUadyRwkJS 170 | [npm-img]: https://img.shields.io/npm/v/postcss-extend-rule.svg 171 | [npm-url]: https://www.npmjs.com/package/postcss-extend-rule 172 | 173 | [CSS Extend Rules Specification]: https://jonathantneal.github.io/specs/css-extend-rule/ 174 | [Functional Selectors]: https://jonathantneal.github.io/specs/css-extend-rule/#functional-selector 175 | [PostCSS]: https://github.com/postcss/postcss 176 | [PostCSS Extend Rule]: https://github.com/csstools/postcss-extend-rule 177 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import nesting from 'postcss-nesting'; 2 | 3 | // functional selector match 4 | const functionalSelectorMatch = /(^|[^\w-])(%[_a-zA-Z]+[_a-zA-Z0-9-]*)([^\w-]|$)/i; 5 | 6 | // plugin 7 | const postcssExtendRule = (rawopts) => { 8 | // options ( onFunctionalSelector, onRecursiveExtend, onUnusedExtend) 9 | const opts = Object(rawopts); 10 | 11 | let extendMatch = /^extend$/i; 12 | if (opts.name instanceof RegExp) { 13 | extendMatch = opts.name; 14 | } else if ('name' in opts) { 15 | extendMatch = new RegExp(`^${opts.name}$`, 'i'); 16 | } 17 | 18 | return { 19 | postcssPlugin: 'postcss-extend-rule', 20 | OnceExit(root, { postcss, result }) { 21 | const extendedAtRules = new WeakMap(); 22 | 23 | // for each extend at-rule 24 | root.walkAtRules(extendMatch, extendAtRule => { 25 | let parent = extendAtRule.parent; 26 | 27 | while (parent.parent && parent.parent !== root) { 28 | parent = parent.parent; 29 | } 30 | 31 | // do not revisit visited extend at-rules 32 | if (!extendedAtRules.has(extendAtRule)) { 33 | extendedAtRules.set(extendAtRule, true); 34 | 35 | // selector identifier 36 | const selectorIdMatch = getSelectorIdMatch(extendAtRule.params, postcss); 37 | 38 | // extending rules 39 | const extendingRules = getExtendingRules(selectorIdMatch, extendAtRule); 40 | 41 | // if there are extending rules 42 | if (extendingRules.length) { 43 | // replace the extend at-rule with the extending rules 44 | extendAtRule.replaceWith(extendingRules); 45 | 46 | // transform any nesting at-rules 47 | const cloneRoot = postcss.root().append(parent.clone()); 48 | 49 | // apply nesting (sync) 50 | postcss([nesting({ noIsPseudoSelector: true })]) 51 | .process(cloneRoot) 52 | .sync(); 53 | 54 | parent.replaceWith(cloneRoot); 55 | } else { 56 | // manage unused extend at-rules 57 | const unusedExtendMessage = `Unused extend at-rule "${extendAtRule.params}"`; 58 | 59 | if (opts.onUnusedExtend === 'throw') { 60 | throw extendAtRule.error(unusedExtendMessage, { word: extendAtRule.name }); 61 | } else if (opts.onUnusedExtend === 'warn') { 62 | extendAtRule.warn(result, unusedExtendMessage); 63 | } else if (opts.onUnusedExtend !== 'ignore') { 64 | extendAtRule.remove(); 65 | } 66 | } 67 | } else { 68 | // manage revisited extend at-rules 69 | const revisitedExtendMessage = `Revisited extend at-rule "${extendAtRule.params}"`; 70 | 71 | if (opts.onRecursiveExtend === 'throw') { 72 | throw extendAtRule.error(revisitedExtendMessage, { word: extendAtRule.name }); 73 | } else if (opts.onRecursiveExtend === 'warn') { 74 | extendAtRule.warn(result, revisitedExtendMessage); 75 | } else if (opts.onRecursiveExtend !== 'ignore') { 76 | extendAtRule.remove(); 77 | } 78 | } 79 | }); 80 | 81 | root.walkRules(functionalSelectorMatch, functionalRule => { 82 | // manage encountered functional selectors 83 | const functionalSelectorMessage = `Encountered functional selector "${functionalRule.selector}"`; 84 | 85 | if (opts.onFunctionalSelector === 'throw') { 86 | throw functionalRule.error(functionalSelectorMessage, { word: functionalRule.selector.match(functionalSelectorMatch)[1] }); 87 | } else if (opts.onFunctionalSelector === 'warn') { 88 | functionalRule.warn(result, functionalSelectorMessage); 89 | } else if (opts.onFunctionalSelector !== 'ignore') { 90 | functionalRule.remove(); 91 | } 92 | }); 93 | }, 94 | }; 95 | }; 96 | 97 | function getExtendingRules(selectorIdMatch, extendAtRule) { 98 | // extending rules 99 | const extendingRules = []; 100 | 101 | // for each rule found from root of the extend at-rule with a matching selector identifier 102 | extendAtRule.root().walkRules(selectorIdMatch, matchingRule => { 103 | // nesting selectors for the selectors matching the selector identifier 104 | const nestingSelectors = matchingRule.selectors.filter( 105 | selector => selectorIdMatch.test(selector), 106 | ).map( 107 | selector => selector.replace(selectorIdMatch, '$1&$3'), 108 | ).join(','); 109 | 110 | // matching rule’s cloned nodes 111 | const nestingNodes = matchingRule.clone().nodes; 112 | 113 | // clone the matching rule as a nested rule 114 | let clone = extendAtRule.clone({ 115 | name: 'nest', 116 | params: nestingSelectors, 117 | nodes: nestingNodes, 118 | // empty the extending rules, as they are likely non-conforming 119 | raws: {}, 120 | }); 121 | 122 | // preserve nesting of parent rules and at-rules 123 | let parent = matchingRule.parent; 124 | 125 | while (parent && (parent.type === 'rule' || parent.type === 'atrule')) { 126 | clone = parent.clone().removeAll().append([ clone ]); 127 | 128 | parent = parent.parent; 129 | } 130 | 131 | // push the matching rule to the extending rules 132 | extendingRules.push(clone); 133 | }); 134 | 135 | // return the extending rules 136 | return extendingRules; 137 | } 138 | 139 | function getSelectorIdMatch(selectorIds, postcss) { 140 | // escape the contents of the selector id to avoid being parsed as regex 141 | const escapedSelectorIds = postcss.list.comma(selectorIds).map( 142 | selectorId => selectorId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 143 | ).join('|'); 144 | 145 | // selector unattached to an existing selector 146 | return new RegExp(`(^|[^\\w-]!.!#)(${escapedSelectorIds})([^\\w-]|$)`, ''); 147 | } 148 | 149 | postcssExtendRule.postcss = true; 150 | 151 | export default postcssExtendRule; 152 | -------------------------------------------------------------------------------- /dist/index.cjs: -------------------------------------------------------------------------------- 1 | var nesting = require('postcss-nesting'); 2 | 3 | // functional selector match 4 | const functionalSelectorMatch = /(^|[^\w-])(%[_a-zA-Z]+[_a-zA-Z0-9-]*)([^\w-]|$)/i; 5 | 6 | // plugin 7 | const postcssExtendRule = rawopts => { 8 | // options ( onFunctionalSelector, onRecursiveExtend, onUnusedExtend) 9 | const opts = Object(rawopts); 10 | let extendMatch = /^extend$/i; 11 | if (opts.name instanceof RegExp) { 12 | extendMatch = opts.name; 13 | } else if ('name' in opts) { 14 | extendMatch = new RegExp(`^${opts.name}$`, 'i'); 15 | } 16 | return { 17 | postcssPlugin: 'postcss-extend-rule', 18 | OnceExit(root, { 19 | postcss, 20 | result 21 | }) { 22 | const extendedAtRules = new WeakMap(); 23 | 24 | // for each extend at-rule 25 | root.walkAtRules(extendMatch, extendAtRule => { 26 | let parent = extendAtRule.parent; 27 | while (parent.parent && parent.parent !== root) { 28 | parent = parent.parent; 29 | } 30 | 31 | // do not revisit visited extend at-rules 32 | if (!extendedAtRules.has(extendAtRule)) { 33 | extendedAtRules.set(extendAtRule, true); 34 | 35 | // selector identifier 36 | const selectorIdMatch = getSelectorIdMatch(extendAtRule.params, postcss); 37 | 38 | // extending rules 39 | const extendingRules = getExtendingRules(selectorIdMatch, extendAtRule); 40 | 41 | // if there are extending rules 42 | if (extendingRules.length) { 43 | // replace the extend at-rule with the extending rules 44 | extendAtRule.replaceWith(extendingRules); 45 | 46 | // transform any nesting at-rules 47 | const cloneRoot = postcss.root().append(parent.clone()); 48 | 49 | // apply nesting (sync) 50 | postcss([nesting({ 51 | noIsPseudoSelector: true 52 | })]).process(cloneRoot).sync(); 53 | parent.replaceWith(cloneRoot); 54 | } else { 55 | // manage unused extend at-rules 56 | const unusedExtendMessage = `Unused extend at-rule "${extendAtRule.params}"`; 57 | if (opts.onUnusedExtend === 'throw') { 58 | throw extendAtRule.error(unusedExtendMessage, { 59 | word: extendAtRule.name 60 | }); 61 | } else if (opts.onUnusedExtend === 'warn') { 62 | extendAtRule.warn(result, unusedExtendMessage); 63 | } else if (opts.onUnusedExtend !== 'ignore') { 64 | extendAtRule.remove(); 65 | } 66 | } 67 | } else { 68 | // manage revisited extend at-rules 69 | const revisitedExtendMessage = `Revisited extend at-rule "${extendAtRule.params}"`; 70 | if (opts.onRecursiveExtend === 'throw') { 71 | throw extendAtRule.error(revisitedExtendMessage, { 72 | word: extendAtRule.name 73 | }); 74 | } else if (opts.onRecursiveExtend === 'warn') { 75 | extendAtRule.warn(result, revisitedExtendMessage); 76 | } else if (opts.onRecursiveExtend !== 'ignore') { 77 | extendAtRule.remove(); 78 | } 79 | } 80 | }); 81 | root.walkRules(functionalSelectorMatch, functionalRule => { 82 | // manage encountered functional selectors 83 | const functionalSelectorMessage = `Encountered functional selector "${functionalRule.selector}"`; 84 | if (opts.onFunctionalSelector === 'throw') { 85 | throw functionalRule.error(functionalSelectorMessage, { 86 | word: functionalRule.selector.match(functionalSelectorMatch)[1] 87 | }); 88 | } else if (opts.onFunctionalSelector === 'warn') { 89 | functionalRule.warn(result, functionalSelectorMessage); 90 | } else if (opts.onFunctionalSelector !== 'ignore') { 91 | functionalRule.remove(); 92 | } 93 | }); 94 | } 95 | }; 96 | }; 97 | function getExtendingRules(selectorIdMatch, extendAtRule) { 98 | // extending rules 99 | const extendingRules = []; 100 | 101 | // for each rule found from root of the extend at-rule with a matching selector identifier 102 | extendAtRule.root().walkRules(selectorIdMatch, matchingRule => { 103 | // nesting selectors for the selectors matching the selector identifier 104 | const nestingSelectors = matchingRule.selectors.filter(selector => selectorIdMatch.test(selector)).map(selector => selector.replace(selectorIdMatch, '$1&$3')).join(','); 105 | 106 | // matching rule’s cloned nodes 107 | const nestingNodes = matchingRule.clone().nodes; 108 | 109 | // clone the matching rule as a nested rule 110 | let clone = extendAtRule.clone({ 111 | name: 'nest', 112 | params: nestingSelectors, 113 | nodes: nestingNodes, 114 | // empty the extending rules, as they are likely non-conforming 115 | raws: {} 116 | }); 117 | 118 | // preserve nesting of parent rules and at-rules 119 | let parent = matchingRule.parent; 120 | while (parent && (parent.type === 'rule' || parent.type === 'atrule')) { 121 | clone = parent.clone().removeAll().append([clone]); 122 | parent = parent.parent; 123 | } 124 | 125 | // push the matching rule to the extending rules 126 | extendingRules.push(clone); 127 | }); 128 | 129 | // return the extending rules 130 | return extendingRules; 131 | } 132 | function getSelectorIdMatch(selectorIds, postcss) { 133 | // escape the contents of the selector id to avoid being parsed as regex 134 | const escapedSelectorIds = postcss.list.comma(selectorIds).map(selectorId => selectorId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'); 135 | 136 | // selector unattached to an existing selector 137 | return new RegExp(`(^|[^\\w-]!.!#)(${escapedSelectorIds})([^\\w-]|$)`, ''); 138 | } 139 | postcssExtendRule.postcss = true; 140 | 141 | module.exports = postcssExtendRule; 142 | -------------------------------------------------------------------------------- /dist/index.mjs: -------------------------------------------------------------------------------- 1 | import nesting from 'postcss-nesting'; 2 | 3 | // functional selector match 4 | const functionalSelectorMatch = /(^|[^\w-])(%[_a-zA-Z]+[_a-zA-Z0-9-]*)([^\w-]|$)/i; 5 | 6 | // plugin 7 | const postcssExtendRule = rawopts => { 8 | // options ( onFunctionalSelector, onRecursiveExtend, onUnusedExtend) 9 | const opts = Object(rawopts); 10 | let extendMatch = /^extend$/i; 11 | if (opts.name instanceof RegExp) { 12 | extendMatch = opts.name; 13 | } else if ('name' in opts) { 14 | extendMatch = new RegExp(`^${opts.name}$`, 'i'); 15 | } 16 | return { 17 | postcssPlugin: 'postcss-extend-rule', 18 | OnceExit(root, { 19 | postcss, 20 | result 21 | }) { 22 | const extendedAtRules = new WeakMap(); 23 | 24 | // for each extend at-rule 25 | root.walkAtRules(extendMatch, extendAtRule => { 26 | let parent = extendAtRule.parent; 27 | while (parent.parent && parent.parent !== root) { 28 | parent = parent.parent; 29 | } 30 | 31 | // do not revisit visited extend at-rules 32 | if (!extendedAtRules.has(extendAtRule)) { 33 | extendedAtRules.set(extendAtRule, true); 34 | 35 | // selector identifier 36 | const selectorIdMatch = getSelectorIdMatch(extendAtRule.params, postcss); 37 | 38 | // extending rules 39 | const extendingRules = getExtendingRules(selectorIdMatch, extendAtRule); 40 | 41 | // if there are extending rules 42 | if (extendingRules.length) { 43 | // replace the extend at-rule with the extending rules 44 | extendAtRule.replaceWith(extendingRules); 45 | 46 | // transform any nesting at-rules 47 | const cloneRoot = postcss.root().append(parent.clone()); 48 | 49 | // apply nesting (sync) 50 | postcss([nesting({ 51 | noIsPseudoSelector: true 52 | })]).process(cloneRoot).sync(); 53 | parent.replaceWith(cloneRoot); 54 | } else { 55 | // manage unused extend at-rules 56 | const unusedExtendMessage = `Unused extend at-rule "${extendAtRule.params}"`; 57 | if (opts.onUnusedExtend === 'throw') { 58 | throw extendAtRule.error(unusedExtendMessage, { 59 | word: extendAtRule.name 60 | }); 61 | } else if (opts.onUnusedExtend === 'warn') { 62 | extendAtRule.warn(result, unusedExtendMessage); 63 | } else if (opts.onUnusedExtend !== 'ignore') { 64 | extendAtRule.remove(); 65 | } 66 | } 67 | } else { 68 | // manage revisited extend at-rules 69 | const revisitedExtendMessage = `Revisited extend at-rule "${extendAtRule.params}"`; 70 | if (opts.onRecursiveExtend === 'throw') { 71 | throw extendAtRule.error(revisitedExtendMessage, { 72 | word: extendAtRule.name 73 | }); 74 | } else if (opts.onRecursiveExtend === 'warn') { 75 | extendAtRule.warn(result, revisitedExtendMessage); 76 | } else if (opts.onRecursiveExtend !== 'ignore') { 77 | extendAtRule.remove(); 78 | } 79 | } 80 | }); 81 | root.walkRules(functionalSelectorMatch, functionalRule => { 82 | // manage encountered functional selectors 83 | const functionalSelectorMessage = `Encountered functional selector "${functionalRule.selector}"`; 84 | if (opts.onFunctionalSelector === 'throw') { 85 | throw functionalRule.error(functionalSelectorMessage, { 86 | word: functionalRule.selector.match(functionalSelectorMatch)[1] 87 | }); 88 | } else if (opts.onFunctionalSelector === 'warn') { 89 | functionalRule.warn(result, functionalSelectorMessage); 90 | } else if (opts.onFunctionalSelector !== 'ignore') { 91 | functionalRule.remove(); 92 | } 93 | }); 94 | } 95 | }; 96 | }; 97 | function getExtendingRules(selectorIdMatch, extendAtRule) { 98 | // extending rules 99 | const extendingRules = []; 100 | 101 | // for each rule found from root of the extend at-rule with a matching selector identifier 102 | extendAtRule.root().walkRules(selectorIdMatch, matchingRule => { 103 | // nesting selectors for the selectors matching the selector identifier 104 | const nestingSelectors = matchingRule.selectors.filter(selector => selectorIdMatch.test(selector)).map(selector => selector.replace(selectorIdMatch, '$1&$3')).join(','); 105 | 106 | // matching rule’s cloned nodes 107 | const nestingNodes = matchingRule.clone().nodes; 108 | 109 | // clone the matching rule as a nested rule 110 | let clone = extendAtRule.clone({ 111 | name: 'nest', 112 | params: nestingSelectors, 113 | nodes: nestingNodes, 114 | // empty the extending rules, as they are likely non-conforming 115 | raws: {} 116 | }); 117 | 118 | // preserve nesting of parent rules and at-rules 119 | let parent = matchingRule.parent; 120 | while (parent && (parent.type === 'rule' || parent.type === 'atrule')) { 121 | clone = parent.clone().removeAll().append([clone]); 122 | parent = parent.parent; 123 | } 124 | 125 | // push the matching rule to the extending rules 126 | extendingRules.push(clone); 127 | }); 128 | 129 | // return the extending rules 130 | return extendingRules; 131 | } 132 | function getSelectorIdMatch(selectorIds, postcss) { 133 | // escape the contents of the selector id to avoid being parsed as regex 134 | const escapedSelectorIds = postcss.list.comma(selectorIds).map(selectorId => selectorId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'); 135 | 136 | // selector unattached to an existing selector 137 | return new RegExp(`(^|[^\\w-]!.!#)(${escapedSelectorIds})([^\\w-]|$)`, ''); 138 | } 139 | postcssExtendRule.postcss = true; 140 | 141 | export { postcssExtendRule as default }; 142 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # CC0 1.0 Universal 2 | 3 | ## Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an “owner”) of an original work of 8 | authorship and/or a database (each, a “Work”). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific works 12 | (“Commons”) that the public can reliably and without fear of later claims of 13 | infringement build upon, modify, incorporate in other works, reuse and 14 | redistribute as freely as possible in any form whatsoever and for any purposes, 15 | including without limitation commercial purposes. These owners may contribute 16 | to the Commons to promote the ideal of a free culture and the further 17 | production of creative, cultural and scientific works, or to gain reputation or 18 | greater distribution for their Work in part through the use and efforts of 19 | others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation of 22 | additional consideration or compensation, the person associating CC0 with a 23 | Work (the “Affirmer”), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and 25 | publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights (“Copyright and 31 | Related Rights”). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 1. the right to reproduce, adapt, distribute, perform, display, communicate, 34 | and translate a Work; 35 | 2. moral rights retained by the original author(s) and/or performer(s); 36 | 3. publicity and privacy rights pertaining to a person’s image or likeness 37 | depicted in a Work; 38 | 4. rights protecting against unfair competition in regards to a Work, 39 | subject to the limitations in paragraph 4(i), below; 40 | 5. rights protecting the extraction, dissemination, use and reuse of data in 41 | a Work; 42 | 6. database rights (such as those arising under Directive 96/9/EC of the 43 | European Parliament and of the Council of 11 March 1996 on the legal 44 | protection of databases, and under any national implementation thereof, 45 | including any amended or successor version of such directive); and 46 | 7. other similar, equivalent or corresponding rights throughout the world 47 | based on applicable law or treaty, and any national implementations 48 | thereof. 49 | 50 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 51 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 52 | unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright 53 | and Related Rights and associated claims and causes of action, whether now 54 | known or unknown (including existing as well as future claims and causes of 55 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 56 | duration provided by applicable law or treaty (including future time 57 | extensions), (iii) in any current or future medium and for any number of 58 | copies, and (iv) for any purpose whatsoever, including without limitation 59 | commercial, advertising or promotional purposes (the “Waiver”). Affirmer 60 | makes the Waiver for the benefit of each member of the public at large and 61 | to the detriment of Affirmer’s heirs and successors, fully intending that 62 | such Waiver shall not be subject to revocation, rescission, cancellation, 63 | termination, or any other legal or equitable action to disrupt the quiet 64 | enjoyment of the Work by the public as contemplated by Affirmer’s express 65 | Statement of Purpose. 66 | 67 | 3. Public License Fallback. Should any part of the Waiver for any reason be 68 | judged legally invalid or ineffective under applicable law, then the Waiver 69 | shall be preserved to the maximum extent permitted taking into account 70 | Affirmer’s express Statement of Purpose. In addition, to the extent the 71 | Waiver is so judged Affirmer hereby grants to each affected person a 72 | royalty-free, non transferable, non sublicensable, non exclusive, 73 | irrevocable and unconditional license to exercise Affirmer’s Copyright and 74 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 75 | maximum duration provided by applicable law or treaty (including future time 76 | extensions), (iii) in any current or future medium and for any number of 77 | copies, and (iv) for any purpose whatsoever, including without limitation 78 | commercial, advertising or promotional purposes (the “License”). The License 79 | shall be deemed effective as of the date CC0 was applied by Affirmer to the 80 | Work. Should any part of the License for any reason be judged legally 81 | invalid or ineffective under applicable law, such partial invalidity or 82 | ineffectiveness shall not invalidate the remainder of the License, and in 83 | such case Affirmer hereby affirms that he or she will not (i) exercise any 84 | of his or her remaining Copyright and Related Rights in the Work or (ii) 85 | assert any associated claims and causes of action with respect to the Work, 86 | in either case contrary to Affirmer’s express Statement of Purpose. 87 | 88 | 4. Limitations and Disclaimers. 89 | 1. No trademark or patent rights held by Affirmer are waived, abandoned, 90 | surrendered, licensed or otherwise affected by this document. 91 | 2. Affirmer offers the Work as-is and makes no representations or warranties 92 | of any kind concerning the Work, express, implied, statutory or 93 | otherwise, including without limitation warranties of title, 94 | merchantability, fitness for a particular purpose, non infringement, or 95 | the absence of latent or other defects, accuracy, or the present or 96 | absence of errors, whether or not discoverable, all to the greatest 97 | extent permissible under applicable law. 98 | 3. Affirmer disclaims responsibility for clearing rights of other persons 99 | that may apply to the Work or any use thereof, including without 100 | limitation any person’s Copyright and Related Rights in the Work. 101 | Further, Affirmer disclaims responsibility for obtaining any necessary 102 | consents, permissions or other rights required for any use of the Work. 103 | 4. Affirmer understands and acknowledges that Creative Commons is not a 104 | party to this document and has no duty or obligation with respect to this 105 | CC0 or use of the Work. 106 | 107 | For more information, please see 108 | http://creativecommons.org/publicdomain/zero/1.0/. 109 | --------------------------------------------------------------------------------