├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── eslint.config.js ├── index.js ├── jest-setup.js ├── package-lock.json ├── package.json ├── rules ├── index.js ├── order │ ├── README.md │ ├── calcAtRulePatternPriority.js │ ├── calcRulePatternPriority.js │ ├── checkNode.js │ ├── checkOrder.js │ ├── createOrderInfo.js │ ├── getDescription.js │ ├── getOrderData.js │ ├── index.js │ ├── messages.js │ ├── ruleName.js │ ├── tests │ │ ├── index.js │ │ └── validate-options.js │ └── validatePrimaryOption.js ├── properties-alphabetical-order │ ├── README.md │ ├── checkChild.js │ ├── checkNode.js │ ├── index.js │ ├── isOrderCorrect.js │ └── tests │ │ └── index.js └── properties-order │ ├── README.md │ ├── addEmptyLineBefore.js │ ├── checkEmptyLineBefore.js │ ├── checkEmptyLineBeforeFirstProp.js │ ├── checkNodeForEmptyLines.js │ ├── checkNodeForOrder.js │ ├── checkOrder.js │ ├── createFlatOrder.js │ ├── createOrderInfo.js │ ├── getNodeData.js │ ├── hasEmptyLineBefore.js │ ├── index.js │ ├── messages.js │ ├── removeEmptyLinesBefore.js │ ├── ruleName.js │ ├── tests │ ├── addEmptyLineBefore.test.js │ ├── createFlatOrder.test.js │ ├── empty-line-before-unspecified.js │ ├── empty-line-before.js │ ├── empty-line-minimum-property-threshold.js │ ├── flat.js │ ├── grouped-flexible.js │ ├── grouped-strict.js │ ├── no-empty-line-between.js │ ├── removeEmptyLineBefore.test.js │ ├── report-when-not-fixed.js │ └── validate-options.js │ └── validatePrimaryOption.js └── utils ├── __tests__ ├── isShorthand.test.js └── vendor.test.js ├── checkAlphabeticalOrder.js ├── getContainingNode.js ├── isAtVariable.js ├── isCustomProperty.js ├── isDollarVariable.js ├── isLessMixin.js ├── isProperty.js ├── isRuleWithNodes.js ├── isShorthand.js ├── isStandardSyntaxProperty.js ├── namespace.js ├── shorthandData.js ├── validateType.js └── vendor.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Autodetect text files 2 | * text=auto 3 | 4 | # Force the following filetypes to have unix eols, so Windows does not break them 5 | *.* text eol=lf 6 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | lint: 13 | name: Lint on Node.js LTS 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 'lts/*' 23 | 24 | - run: npm ci 25 | 26 | - name: Lint 27 | run: npm run lint 28 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | test: 13 | name: Test on Node.js ${{ matrix.node }} 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | node: [20, 'lts/*'] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node }} 28 | 29 | - run: npm ci 30 | 31 | - name: Test 32 | run: npm test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | dev/ 4 | coverage/ 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix = "" 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](https://semver.org/). 5 | 6 | ## 7.0.0 7 | * Dropped Node.js 18 support 8 | * Dropped support for Stylelint older than 16.18.0 9 | * Changed: Ignore properties case for `properties-order` and `properties-alphabetical-order` 10 | * Added support for more properties shorthands 11 | * Fixed `order` autofix not applied, when Stylelint disable comments are present 12 | * Fixed `properties-alphabetical-order` autofix not applied, when Stylelint disable comments are present 13 | * Fixed `properties-order` autofix not applied, when Stylelint disable comments are present 14 | * Fixed: Don't apply `properties-alphabetical-order` autofixing if there are no violations 15 | * Fixed: Report warnings if they weren't fixed by autofix in `properties-alphabetical-order` 16 | 17 | ## 6.0.4 18 | * Added support for Stylelint 16 19 | 20 | ## 6.0.3 21 | * Fixed sorting inside CSS-in-JS `css` helper 22 | 23 | ## 6.0.2 24 | * Added Stylelint v15 to peerDependencies 25 | 26 | ## 6.0.1 27 | * Fix regression causing root of CSS or SCSS to report violations 28 | 29 | ## 6.0.0 30 | * Dropped Node.js 12 and 14 support. 31 | * Added support for `postcss-styled-syntax`. 32 | 33 | ## 5.0.0 34 | 35 | * Breaking change: Dropped Node.js 8 support. Node.js 12 or greater is now required. 36 | * Breaking change: Dropped support for Stylelint 13 and earlier. 37 | * Added support for Stylelint 14. 38 | 39 | ## 4.1.0 40 | 41 | * Added `name` option to extended rule object to improve error messaging (for `order`). 42 | * Fixed `order` not reporting warnings, if autofix didn't fix them. 43 | 44 | ## 4.0.0 45 | 46 | * Breaking change: Dropped Node.js 8 support. Node.js 10 or greater is now required. 47 | * Breaking change: Always remove empty line before the first property if this property has any `emptyLineBefore*` option targeting it in `properties-order`. Even if option set to `always` empty line before the first property will be removed. 48 | * Fixed false positives for `emptyLineBeforeUnspecified`. 49 | 50 | ## 3.1.1 51 | 52 | * Added `stylelint@11` as a peer dependency. 53 | 54 | ## 3.1.0 55 | 56 | * Added `emptyLineBefore: "threshold"` option, and related options (`emptyLineMinimumPropertyThreshold`, `emptyLineBeforeUnspecified: "threshold"`) to `properties-order`. 57 | 58 | ## 3.0.1 59 | 60 | * Fixed `properties-order` not report warnings, if autofix didn't fix them. 61 | * Fixed `properties-alphabetical-order` now puts shorthands before their longhand forms even if that isn't alphabetical to avoid broken CSS. E. g. `border-color` will be before `border-bottom-color`. 62 | 63 | ## 3.0.0 64 | 65 | * Dropped Node.js 6 support. Node.js 8.7.0 or greater is now required. 66 | * Removed stylelint@9 as a peer dependency. stylelint 10 or greater is now required. 67 | * Added `emptyLineBeforeUnspecified` option for `properties-order`. 68 | 69 | ## 2.2.1 70 | 71 | * Fixed false negatives with `noEmptyLineBetween` in combination with the `order: "flexible"`. 72 | 73 | ## 2.2.0 74 | 75 | * Added `noEmptyLineBetween` for groups in `properties-order`. 76 | * Added `stylelint@10` as a peer dependency. 77 | 78 | ## 2.1.0 79 | 80 | * Added _experimental_ support for HTML style tag and attribute. 81 | * Added _experimental_ support for CSS-in-JS. 82 | 83 | ## 2.0.0 84 | 85 | This is a major release, because this plugin requires stylelint@9.8.0+ to work correctly with Less files. 86 | 87 | * Added optional groupName property for properties-order. 88 | * Adopted `postcss-less@3` parser changes, which is dependency of `stylelint@9.7.0+`. 89 | * Fixed incorrect fixing when properties order and empty lines should be changed at the same time. 90 | 91 | ## 1.0.0 92 | 93 | * Removed `stylelint@8` as a peer dependency. 94 | 95 | ## 0.8.1 96 | 97 | * Add `stylelint@9.0.0` as a peer dependency. 98 | 99 | ## 0.8.0 100 | 101 | * Breaking change: Dropped Node.js 4 support. Use Node.js 6 or newer. 102 | * Changed: `order` and `properties-order` will no longer autofix proactively. If there no violations would be reported with autofix disabled, then nothing will be changed with autofix enabled. Previously, there were changes to `flexible` properties order ([#49](https://github.com/hudochenkov/stylelint-order/issues/49)) or to the order of content within declaration blocks ([#51](https://github.com/hudochenkov/stylelint-order/issues/51)). 103 | 104 | ## 0.7.0 105 | 106 | * Specified `stylelint` in `peerDependencies` rather in `dependencies`. Following [stylelint's plugin guide](https://github.com/stylelint/stylelint/blob/master/docs/developer-guide/plugins.md#peer-dependencies). 107 | 108 | ## 0.6.0 109 | 110 | * Migrated to `stylelint@8.0.0`. 111 | 112 | ## 0.5.0 113 | * Added autofixing for every rule! Please read docs before using this feature, because each rule has some caveats. stylelint 7.11+ is required for this feature. 114 | * Removed SCSS nested properties support. 115 | * Removed property shortcuts in `properties-order`. Before this version it was possible to define only e.g. `padding` and it would define position for all undefined `padding-*` properties. Now every property should be explicitly defined in a config. 116 | * Removed deprecation warnings: 117 | * `declaration-block-order` 118 | * `declaration-block-properties-order` 119 | * `declaration-block-properties-alphabetical-order` 120 | * `declaration-block-properties-specified-order` 121 | * `declaration-block-property-groups-structure` 122 | 123 | ## 0.4.4 124 | * Fixed false negative for blockless at-rules in `order`. 125 | 126 | ## 0.4.3 127 | * Fixed regression in `properties-order` introduced in 0.4.2. 128 | 129 | ## 0.4.2 130 | * Fixed: `order` and `properties-order` weren't recognize SCSS nested properties as declarations. 131 | 132 | ## 0.4.1 133 | * Fixed `properties-order` bug, when non-standard declaration is following after a standard one 134 | 135 | ## 0.4.0 136 | * Removed `declaration-block-properties-specified-order`. Instead use `properties-order` rule. 137 | * Removed `declaration-block-property-groups-structure`. Instead use `properties-order` rule. 138 | * Renamed `declaration-block-order` to `order` 139 | * Renamed `declaration-block-properties-alphabetical-order` to `properties-alphabetical-order` 140 | * Added `properties-order` rule. It combines removed `declaration-block-properties-specified-order`, `declaration-block-property-groups-structure`, and now support flexible order. Basically it's like [`declaration-block-properties-order` in stylelint 6.5.0](https://github.com/stylelint/stylelint/tree/6.5.0/src/rules/declaration-block-properties-order), but better :) 141 | 142 | ## 0.3.0 143 | * Changed: Breaking! `declaration-block-property-groups-structure` now uses `declaration-block-properties-specified-order` rather stylelint's deprecated `declaration-block-properties-order`. Flexible group order isn't supported anymore 144 | * Added: `declaration-block-order` support new `rule` extended object, which have new `selector` option. Rules in order can be specified by their selector 145 | * Added: New keyword `at-variables` in `declaration-block-order` 146 | * Added: New keyword `less-mixins` in `declaration-block-order` 147 | 148 | ## 0.2.2 149 | * Fixed tests for `declaration-block-property-groups-structure` which were broken by previous fix ¯\_(ツ)_/¯ 150 | 151 | ## 0.2.1 152 | * Fixed incorrect severity level for `declaration-block-properties-order` which is called from `declaration-block-property-groups-structure` 153 | 154 | ## 0.2.0 155 | * Breaking: Renamed `property-groups-structure` to `declaration-block-property-groups-structure` 156 | * Added `declaration-block-properties-specified-order` rule 157 | * Fixed unavailability of `declaration-block-properties-alphabetical-order` rule 158 | 159 | ## 0.1.0 160 | * Initial release. 161 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2016–present Aleks Hudochenkov 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 | # stylelint-order 2 | 3 | [![npm version][npm-version-img]][npm] [![npm downloads last month][npm-downloads-img]][npm] 4 | 5 | A plugin pack of order-related linting rules for [Stylelint]. Every rule supports autofixing (`stylelint --fix`). 6 | 7 | ## Installation 8 | 9 | 1. If you haven't, install [Stylelint]: 10 | 11 | ``` 12 | npm install stylelint --save-dev 13 | ``` 14 | 15 | 2. Install `stylelint-order`: 16 | 17 | ``` 18 | npm install stylelint-order --save-dev 19 | ``` 20 | 21 | ## Usage 22 | 23 | Add `stylelint-order` to your Stylelint config `plugins` array, then add rules you need to the rules list. All rules from stylelint-order need to be namespaced with `order`. 24 | 25 | ```json 26 | { 27 | "plugins": [ 28 | "stylelint-order" 29 | ], 30 | "rules": { 31 | "order/order": [ 32 | "custom-properties", 33 | "declarations" 34 | ], 35 | "order/properties-order": [ 36 | "width", 37 | "height" 38 | ] 39 | } 40 | } 41 | ``` 42 | 43 | ## Rules 44 | 45 | * [`order`](./rules/order/README.md): Specify the order of content within declaration blocks. 46 | * [`properties-order`](./rules/properties-order/README.md): Specify the order of properties within declaration blocks. 47 | * [`properties-alphabetical-order`](./rules/properties-alphabetical-order/README.md): Specify the alphabetical order of properties within declaration blocks. 48 | 49 | ## Autofixing 50 | 51 | Every rule supports autofixing with `stylelint --fix`. [postcss-sorting] is used internally for order autofixing. 52 | 53 | Automatic sorting has some limitations that are described for every rule, if any. Please, take a look at [how comments are handled](https://github.com/hudochenkov/postcss-sorting#handling-comments) by `postcss-sorting`. 54 | 55 | CSS-in-JS styles with template interpolation [could be ignored by autofixing](https://github.com/hudochenkov/postcss-sorting#css-in-js) to avoid style corruption. 56 | 57 | Autofixing in Less syntax may work but isn't officially supported. 58 | 59 | ## Example configs 60 | 61 | All these configs have `properties-order` configured with logical properties groups: 62 | 63 | * [`stylelint-config-idiomatic-order`](https://github.com/ream88/stylelint-config-idiomatic-order) 64 | * [`stylelint-config-hudochenkov/order`](https://github.com/hudochenkov/stylelint-config-hudochenkov/blob/master/order.js) 65 | * [`stylelint-config-recess-order`](https://github.com/stormwarning/stylelint-config-recess-order) 66 | * [`stylelint-config-property-sort-order-smacss`](https://github.com/cahamilton/stylelint-config-property-sort-order-smacss) 67 | * [`stylelint-config-clean-order`](https://github.com/kutsan/stylelint-config-clean-order) 68 | 69 | ## Thanks 70 | 71 | `properties-order` and `properties-alphabetical-order` code and README were based on the `declaration-block-properties-order` rule which was a core rule prior to Stylelint 8.0.0. 72 | 73 | [npm-version-img]: https://img.shields.io/npm/v/stylelint-order.svg 74 | [npm-downloads-img]: https://img.shields.io/npm/dm/stylelint-order.svg 75 | [npm]: https://www.npmjs.com/package/stylelint-order 76 | [Stylelint]: https://stylelint.io/ 77 | [postcss-sorting]: https://github.com/hudochenkov/postcss-sorting 78 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { configs } from 'eslint-config-hudochenkov'; 2 | import eslintConfigPrettier from 'eslint-config-prettier'; 3 | import globals from 'globals'; 4 | 5 | export default [ 6 | ...configs.main, 7 | eslintConfigPrettier, 8 | { 9 | languageOptions: { 10 | globals: { 11 | ...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, 'off'])), 12 | ...globals.node, 13 | ...globals.jest, 14 | testConfig: true, 15 | testRule: true, 16 | }, 17 | ecmaVersion: 'latest', 18 | sourceType: 'module', 19 | }, 20 | rules: { 21 | 'import/extensions': [ 22 | 'error', 23 | 'always', 24 | { 25 | ignorePackages: true, 26 | }, 27 | ], 28 | 'unicorn/prefer-module': 'error', 29 | 'unicorn/prefer-node-protocol': 'error', 30 | }, 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import { rules } from './rules/index.js'; 3 | import { namespace } from './utils/namespace.js'; 4 | 5 | const rulesPlugins = Object.keys(rules).map((ruleName) => { 6 | return stylelint.createPlugin(namespace(ruleName), rules[ruleName]); 7 | }); 8 | 9 | // eslint-disable-next-line import/no-default-export 10 | export default rulesPlugins; 11 | -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | // eslint-disable-next-line import/no-extraneous-dependencies 3 | import { getTestRule } from 'jest-preset-stylelint'; 4 | 5 | global.testRule = getTestRule({ plugins: ['./'] }); 6 | 7 | global.testConfig = (input) => { 8 | let testFn; 9 | 10 | if (input.only) { 11 | testFn = test.only; 12 | } else if (input.skip) { 13 | testFn = test.skip; 14 | } else { 15 | testFn = test; 16 | } 17 | 18 | testFn(input.description, () => { 19 | const config = { 20 | plugins: ['./'], 21 | rules: { 22 | [input.ruleName]: input.config, 23 | }, 24 | }; 25 | 26 | return stylelint 27 | .lint({ 28 | code: '', 29 | config, 30 | }) 31 | .then((data) => { 32 | const { invalidOptionWarnings } = data.results[0]; 33 | 34 | if (input.valid) { 35 | expect(invalidOptionWarnings.length).toBe(0); 36 | } else { 37 | expect(invalidOptionWarnings[0].text).toBe(input.message); 38 | } 39 | }); 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stylelint-order", 3 | "version": "7.0.0", 4 | "description": "A collection of order related linting rules for Stylelint.", 5 | "keywords": [ 6 | "stylelint-plugin", 7 | "stylelint", 8 | "css", 9 | "lint", 10 | "order" 11 | ], 12 | "author": "Aleks Hudochenkov ", 13 | "license": "MIT", 14 | "repository": "hudochenkov/stylelint-order", 15 | "files": [ 16 | "rules", 17 | "utils", 18 | "!**/tests", 19 | "!**/__tests__", 20 | "index.js", 21 | "!.DS_Store" 22 | ], 23 | "type": "module", 24 | "exports": "./index.js", 25 | "engines": { 26 | "node": ">=20.19.0" 27 | }, 28 | "dependencies": { 29 | "postcss": "^8.5.3", 30 | "postcss-sorting": "^9.1.0" 31 | }, 32 | "peerDependencies": { 33 | "stylelint": "^16.18.0" 34 | }, 35 | "devDependencies": { 36 | "eslint": "^9.24.0", 37 | "eslint-config-hudochenkov": "^11.0.0", 38 | "eslint-config-prettier": "^10.1.2", 39 | "globals": "^16.0.0", 40 | "husky": "^9.1.7", 41 | "jest": "^29.7.0", 42 | "jest-light-runner": "^0.7.4", 43 | "jest-preset-stylelint": "^7.3.0", 44 | "jest-watch-typeahead": "^2.2.2", 45 | "lint-staged": "^15.5.1", 46 | "postcss-html": "^1.8.0", 47 | "postcss-less": "^6.0.0", 48 | "postcss-styled-syntax": "^0.7.1", 49 | "prettier": "~3.5.3", 50 | "prettier-config-hudochenkov": "^0.4.0", 51 | "stylelint": "^16.18.0" 52 | }, 53 | "scripts": { 54 | "lint": "eslint . --max-warnings 0 && prettier '**/*.js' --check", 55 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", 56 | "watch": "npm run test -- --watch", 57 | "coverage": "npm run test -- --coverage", 58 | "fix": "eslint . --fix --max-warnings 0 && prettier '**/*.js' --write", 59 | "prepare": "husky" 60 | }, 61 | "lint-staged": { 62 | "*.js": [ 63 | "eslint --fix --max-warnings 0", 64 | "prettier --write" 65 | ] 66 | }, 67 | "jest": { 68 | "runner": "jest-light-runner", 69 | "preset": "jest-preset-stylelint", 70 | "setupFiles": [ 71 | "./jest-setup.js" 72 | ], 73 | "watchPlugins": [ 74 | "jest-watch-typeahead/filename", 75 | "jest-watch-typeahead/testname" 76 | ], 77 | "testEnvironment": "node", 78 | "testRegex": ".*\\.test\\.js$|rules/.*/tests/.*\\.js$" 79 | }, 80 | "prettier": "prettier-config-hudochenkov" 81 | } 82 | -------------------------------------------------------------------------------- /rules/index.js: -------------------------------------------------------------------------------- 1 | import { rule as order } from './order/index.js'; 2 | import { rule as propertiesOrder } from './properties-order/index.js'; 3 | import { rule as propertiesAlphabeticalOrder } from './properties-alphabetical-order/index.js'; 4 | 5 | export const rules = { 6 | order, 7 | 'properties-order': propertiesOrder, 8 | 'properties-alphabetical-order': propertiesAlphabeticalOrder, 9 | }; 10 | -------------------------------------------------------------------------------- /rules/order/README.md: -------------------------------------------------------------------------------- 1 | # order 2 | 3 | Specify the order of content within declaration blocks. 4 | 5 | * Options 6 | * [Extended at-rule objects](#extended-at-rule-objects) 7 | * [Extended rule objects](#extended-rule-objects) 8 | * Optional secondary options 9 | * [`unspecified`](#unspecified) 10 | * [Autofixing caveats](#autofixing-caveats) 11 | * [Examples](#examples) 12 | 13 | ## Options 14 | 15 | ```ts 16 | type PrimaryOption = Array; 17 | 18 | type Keyword = 19 | | 'custom-properties' 20 | | 'dollar-variables' 21 | | 'at-variables' 22 | | 'declarations' 23 | | 'rules' 24 | | 'at-rules' 25 | | 'less-mixins'; 26 | 27 | type AtRule = { 28 | type: 'at-rule'; 29 | name?: string; 30 | parameter?: string | RegExp; 31 | hasBlock?: boolean; 32 | }; 33 | 34 | type Rule = { 35 | type: 'rule'; 36 | selector?: string | RegExp; 37 | name?: string; 38 | }; 39 | ``` 40 | 41 | Within an order array, you can include: 42 | 43 | - keywords: 44 | - `custom-properties` — Custom properties (e. g., `--property: 10px;`) 45 | - `dollar-variables` — Dollar variables (e. g., `$variable`) 46 | - `at-variables` — At-variables (e. g., `@variable` available in Less syntax) 47 | - `declarations` — CSS declarations (e. g., `display: block`) 48 | - `rules` — Nested rules (e. g., `a { span {} }`) 49 | - `at-rules` — Nested at-rules (e. g., `div { @media () {} }`) 50 | - `less-mixins` — Mixins in Less syntax (e. g., `.mixin();`) 51 | - extended at-rule objects: 52 | 53 | ```json 54 | { 55 | "type": "at-rule", 56 | "name": "include", 57 | "parameter": "hello", 58 | "hasBlock": true 59 | } 60 | ``` 61 | 62 | - extended rule objects: 63 | 64 | ```json 65 | { 66 | "type": "rule", 67 | "selector": "div", 68 | "name": "tag selector" 69 | } 70 | ``` 71 | 72 | **By default, unlisted elements will be ignored.** So if you specify an array and do not include `declarations`, that means that all declarations can be included before or after any other element. _This can be changed with the `unspecified` option (see below)._ 73 | 74 | ### Extended at-rule objects 75 | 76 | Extended at-rule objects have different parameters and variations. 77 | 78 | Object parameters: 79 | 80 | * `type`: always `"at-rule"` 81 | * `name`: `string`. E. g., `name: "include"` for `@include` 82 | * `parameter`: `string | RegExp`. A string will be translated into a RegExp — `new RegExp(yourString)` — so _be sure to escape properly_. E. g., `parameter: "icon"` for `@include icon(20px);` 83 | * `hasBlock`: `boolean`. E. g., `hasBlock: true` for `@include icon { color: red; }` and not for `@include icon;` 84 | 85 | Always specify `name` if `parameter` is specified. 86 | 87 | Matches all at-rules: 88 | 89 | ```json 90 | { 91 | "type": "at-rule" 92 | } 93 | ``` 94 | 95 | Or keyword `at-rules`. 96 | 97 | Matches all at-rules, which have nested elements: 98 | 99 | ```json 100 | { 101 | "type": "at-rule", 102 | "hasBlock": true 103 | } 104 | ``` 105 | 106 | Matches all at-rules with specific name: 107 | 108 | ```json 109 | { 110 | "type": "at-rule", 111 | "name": "media" 112 | } 113 | ``` 114 | 115 | Matches all at-rules with specific name, which have nested elements: 116 | 117 | ```json 118 | { 119 | "type": "at-rule", 120 | "name": "media", 121 | "hasBlock": true 122 | } 123 | ``` 124 | 125 | Matches all at-rules with specific name and parameter: 126 | 127 | ```json 128 | { 129 | "type": "at-rule", 130 | "name": "include", 131 | "parameter": "icon" 132 | } 133 | ``` 134 | 135 | Matches all at-rules with specific name and parameter, which have nested elements: 136 | 137 | ```json 138 | { 139 | "type": "at-rule", 140 | "name": "include", 141 | "parameter": "icon", 142 | "hasBlock": true 143 | } 144 | ``` 145 | 146 | Each described above variant has more priority than its previous variant. For example, `{ "type": "at-rule", "name": "media" }` will be applied to an element if both `{ "type": "at-rule", "name": "media" }` and `{ "type": "at-rule", "hasBlock": true }` can be applied to an element. 147 | 148 | ### Extended rule objects 149 | 150 | Object parameters: 151 | 152 | * `type`: always `"rule"` 153 | * `selector`: `string | RegExp`. Selector pattern. A string will be translated into a RegExp — `new RegExp(yourString)` — so _be sure to escape properly_. Examples: 154 | * `selector: /^&:[\w-]+$/` matches simple pseudo-classes. E. g., `&:hover`, `&:first-child`. Doesn't match complex pseudo-classes, e. g. `&:not(.is-visible)`. 155 | * `selector: /^&::[\w-]+$/` matches pseudo-elements. E. g. `&::before`, `&::placeholder`. 156 | * `name`: `string`. Selector name (optional). Will be used in error output to help identify extended rule object. 157 | 158 | Matches all rules: 159 | 160 | ```json 161 | { 162 | "type": "rule" 163 | } 164 | ``` 165 | 166 | Or keyword `rules`. 167 | 168 | Matches all rules with selector matching pattern: 169 | 170 | ```json 171 | { 172 | "type": "rule", 173 | "selector": "div" 174 | } 175 | ``` 176 | 177 | ```json 178 | { 179 | "type": "rule", 180 | "selector": "/^&:\\w+$/" 181 | } 182 | ``` 183 | 184 | ## Optional secondary options 185 | 186 | ```ts 187 | type SecondaryOptions = { 188 | unspecified?: "top" | "bottom" | "ignore"; 189 | }; 190 | ``` 191 | 192 | ### `unspecified` 193 | 194 | Value type: `"top" | "bottom" | "ignore"`.
195 | Default value: `"ignore"`. 196 | 197 | Default behavior is the same as `"ignore"`: an unspecified element can appear before or after any other property. 198 | 199 | With `"top"`, unspecified elements are expected _before_ any specified properties. With `"bottom"`, unspecified properties are expected _after_ any specified properties. 200 | 201 | ## Autofixing caveats 202 | 203 | Keyword `less-mixins` aren't supported. 204 | 205 | `unspecified` secondary option is always set to `bottom`. 206 | 207 | ## Examples 208 | 209 | Given: 210 | 211 | ```json 212 | { 213 | "order/order": [ 214 | "custom-properties", 215 | "dollar-variables", 216 | "declarations", 217 | "rules", 218 | "at-rules" 219 | ] 220 | } 221 | ``` 222 | 223 | The following patterns are considered warnings: 224 | 225 | ```css 226 | a { 227 | top: 0; 228 | --height: 10px; 229 | color: pink; 230 | } 231 | ``` 232 | 233 | ```css 234 | a { 235 | @media (min-width: 100px) {} 236 | display: none; 237 | } 238 | ``` 239 | 240 | The following patterns are _not_ considered warnings: 241 | 242 | ```css 243 | a { 244 | --width: 10px; 245 | $height: 20px; 246 | display: none; 247 | span {} 248 | @media (min-width: 100px) {} 249 | } 250 | ``` 251 | 252 | ```css 253 | a { 254 | --height: 10px; 255 | color: pink; 256 | top: 0; 257 | } 258 | ``` 259 | 260 | --- 261 | 262 | Given: 263 | 264 | ```json 265 | { 266 | "order/order": [ 267 | { 268 | "type": "at-rule", 269 | "name": "include" 270 | }, 271 | { 272 | "type": "at-rule", 273 | "name": "include", 274 | "hasBlock": true 275 | }, 276 | { 277 | "type": "at-rule", 278 | "hasBlock": true 279 | }, 280 | { 281 | "type": "at-rule" 282 | } 283 | ] 284 | } 285 | ``` 286 | 287 | The following patterns are considered warnings: 288 | 289 | ```scss 290 | a { 291 | @include hello { 292 | display: block; 293 | } 294 | @include hello; 295 | } 296 | ``` 297 | 298 | ```scss 299 | a { 300 | @extend .something; 301 | @media (min-width: 10px) { 302 | display: none; 303 | } 304 | } 305 | ``` 306 | 307 | The following patterns are _not_ considered warnings: 308 | 309 | ```scss 310 | a { 311 | @include hello; 312 | @include hello { 313 | display: block; 314 | } 315 | @media (min-width: 10px) { 316 | display: none; 317 | } 318 | @extend .something; 319 | } 320 | ``` 321 | 322 | ```scss 323 | a { 324 | @include hello { 325 | display: block; 326 | } 327 | @extend .something; 328 | } 329 | ``` 330 | 331 | --- 332 | 333 | Given: 334 | 335 | ```json 336 | { 337 | "order/order": [ 338 | { 339 | "type": "at-rule", 340 | "name": "include", 341 | "hasBlock": true 342 | }, 343 | { 344 | "type": "at-rule", 345 | "name": "include", 346 | "parameter": "icon", 347 | "hasBlock": true 348 | }, 349 | { 350 | "type": "at-rule", 351 | "name": "include", 352 | "parameter": "icon" 353 | } 354 | ] 355 | } 356 | ``` 357 | 358 | The following patterns are considered warnings: 359 | 360 | ```scss 361 | a { 362 | @include icon { 363 | display: block; 364 | } 365 | @include hello { 366 | display: none; 367 | } 368 | @include icon; 369 | } 370 | ``` 371 | 372 | ```scss 373 | a { 374 | @include icon; 375 | @include icon { 376 | display: block; 377 | } 378 | } 379 | ``` 380 | 381 | The following patterns are _not_ considered warnings: 382 | 383 | ```scss 384 | a { 385 | @include hello { 386 | display: none; 387 | } 388 | @include icon { 389 | display: block; 390 | } 391 | @include icon; 392 | } 393 | ``` 394 | 395 | ```scss 396 | a { 397 | @include hello { 398 | display: none; 399 | } 400 | @include icon; 401 | } 402 | ``` 403 | 404 | --- 405 | 406 | Given: 407 | 408 | ```json 409 | { 410 | "order/order": [ 411 | "custom-properties", 412 | { 413 | "type": "at-rule", 414 | "hasBlock": true 415 | }, 416 | "declarations" 417 | ] 418 | } 419 | ``` 420 | 421 | The following patterns are considered warnings: 422 | 423 | ```css 424 | a { 425 | @media (min-width: 10px) { 426 | display: none; 427 | } 428 | --height: 10px; 429 | width: 20px; 430 | } 431 | ``` 432 | 433 | ```css 434 | a { 435 | width: 20px; 436 | @media (min-width: 10px) { 437 | display: none; 438 | } 439 | --height: 10px; 440 | } 441 | ``` 442 | 443 | ```css 444 | a { 445 | width: 20px; 446 | @media (min-width: 10px) { 447 | display: none; 448 | } 449 | } 450 | ``` 451 | 452 | The following patterns are _not_ considered warnings: 453 | 454 | ```css 455 | a { 456 | --height: 10px; 457 | @media (min-width: 10px) { 458 | display: none; 459 | } 460 | width: 20px; 461 | } 462 | ``` 463 | 464 | ```css 465 | a { 466 | @media (min-width: 10px) { 467 | display: none; 468 | } 469 | width: 20px; 470 | } 471 | ``` 472 | 473 | ```css 474 | a { 475 | --height: 10px; 476 | width: 20px; 477 | } 478 | ``` 479 | 480 | --- 481 | 482 | Given: 483 | 484 | ```json 485 | { 486 | "order/order": [ 487 | { 488 | "type": "rule", 489 | "selector": "^a" 490 | }, 491 | { 492 | "type": "rule", 493 | "selector": "/^&/" 494 | }, 495 | "rules" 496 | ] 497 | } 498 | ``` 499 | 500 | The following patterns are considered warnings: 501 | 502 | ```scss 503 | a { 504 | a {} 505 | &:hover {} 506 | abbr {} 507 | span {} 508 | } 509 | ``` 510 | 511 | ```scss 512 | a { 513 | span {} 514 | &:hover {} 515 | } 516 | ``` 517 | 518 | ```scss 519 | a { 520 | span {} 521 | abbr {} 522 | } 523 | ``` 524 | 525 | The following patterns are _not_ considered warnings: 526 | 527 | ```scss 528 | a { 529 | a {} 530 | abbr {} 531 | &:hover {} 532 | span {} 533 | } 534 | ``` 535 | 536 | ```scss 537 | a { 538 | abbr {} 539 | a {} 540 | } 541 | ``` 542 | 543 | ```scss 544 | a { 545 | abbr {} 546 | span {} 547 | } 548 | ``` 549 | 550 | --- 551 | 552 | Given: 553 | 554 | ```json 555 | { 556 | "order/order": [ 557 | { 558 | "type": "rule", 559 | "selector": "/^&/" 560 | }, 561 | { 562 | "type": "rule", 563 | "selector": "/^&:\\w/" 564 | } 565 | ] 566 | } 567 | ``` 568 | 569 | The following patterns are _not_ considered warnings: 570 | 571 | ```scss 572 | a { 573 | &:hover {} 574 | & b {} 575 | } 576 | ``` 577 | 578 | ```scss 579 | a { 580 | & b {} 581 | &:hover {} 582 | } 583 | ``` 584 | 585 | --- 586 | 587 | Given: 588 | 589 | ```json 590 | { 591 | "order/order": [ 592 | { 593 | "type": "rule", 594 | "selector": "/^&:\\w/" 595 | }, 596 | { 597 | "type": "rule", 598 | "selector": "/^&/" 599 | } 600 | ] 601 | } 602 | ``` 603 | 604 | The following pattern is considered warnings: 605 | 606 | ```scss 607 | a { 608 | & b {} 609 | &:hover {} 610 | } 611 | ``` 612 | 613 | The following pattern is _not_ considered warnings: 614 | 615 | ```scss 616 | a { 617 | &:hover {} 618 | & b {} 619 | } 620 | ``` 621 | 622 | --- 623 | 624 | Given: 625 | 626 | ```json 627 | { 628 | "order/order": [ 629 | [ 630 | "declarations" 631 | ], 632 | { 633 | "unspecified": "ignore" 634 | } 635 | ] 636 | } 637 | ``` 638 | 639 | The following patterns are _not_ considered warnings: 640 | 641 | ```css 642 | a { 643 | --height: 10px; 644 | display: none; 645 | $width: 20px; 646 | } 647 | ``` 648 | 649 | ```css 650 | a { 651 | --height: 10px; 652 | $width: 20px; 653 | display: none; 654 | } 655 | ``` 656 | 657 | ```css 658 | a { 659 | display: none; 660 | --height: 10px; 661 | $width: 20px; 662 | } 663 | ``` 664 | 665 | --- 666 | 667 | Given: 668 | 669 | ```json 670 | { 671 | "order/order": [ 672 | [ 673 | "declarations" 674 | ], 675 | { 676 | "unspecified": "top" 677 | } 678 | ] 679 | } 680 | ``` 681 | 682 | The following patterns are considered warnings: 683 | 684 | ```css 685 | a { 686 | display: none; 687 | --height: 10px; 688 | $width: 20px; 689 | } 690 | ``` 691 | 692 | ```css 693 | a { 694 | --height: 10px; 695 | display: none; 696 | $width: 20px; 697 | } 698 | ``` 699 | 700 | The following patterns are _not_ considered warnings: 701 | 702 | ```css 703 | a { 704 | --height: 10px; 705 | $width: 20px; 706 | display: none; 707 | } 708 | ``` 709 | 710 | ```css 711 | a { 712 | $width: 20px; 713 | --height: 10px; 714 | display: none; 715 | } 716 | ``` 717 | 718 | --- 719 | 720 | Given: 721 | 722 | ```json 723 | { 724 | "order/order": [ 725 | [ 726 | "declarations" 727 | ], 728 | { 729 | "unspecified": "bottom" 730 | } 731 | ] 732 | } 733 | ``` 734 | 735 | The following patterns are considered warnings: 736 | 737 | ```css 738 | a { 739 | --height: 10px; 740 | $width: 20px; 741 | display: none; 742 | } 743 | ``` 744 | 745 | ```css 746 | a { 747 | --height: 10px; 748 | display: none; 749 | $width: 20px; 750 | } 751 | ``` 752 | 753 | The following patterns are _not_ considered warnings: 754 | 755 | ```css 756 | a { 757 | display: none; 758 | --height: 10px; 759 | $width: 20px; 760 | } 761 | ``` 762 | 763 | ```css 764 | a { 765 | display: none; 766 | $width: 20px; 767 | --height: 10px; 768 | } 769 | ``` 770 | -------------------------------------------------------------------------------- /rules/order/calcAtRulePatternPriority.js: -------------------------------------------------------------------------------- 1 | export function calcAtRulePatternPriority(pattern, node) { 2 | // 0 — it pattern doesn't match 3 | // 1 — pattern without `name` and `hasBlock` 4 | // 10010 — pattern match `hasBlock` 5 | // 10100 — pattern match `name` 6 | // 20110 — pattern match `name` and `hasBlock` 7 | // 21100 — patter match `name` and `parameter` 8 | // 31110 — patter match `name`, `parameter`, and `hasBlock` 9 | 10 | let priority = 0; 11 | 12 | // match `hasBlock` 13 | if (pattern.hasOwnProperty('hasBlock') && node.hasBlock === pattern.hasBlock) { 14 | priority += 10; 15 | priority += 10000; 16 | } 17 | 18 | // match `name` 19 | if (pattern.hasOwnProperty('name') && node.name === pattern.name) { 20 | priority += 100; 21 | priority += 10000; 22 | } 23 | 24 | // match `name` 25 | if (pattern.hasOwnProperty('parameter') && pattern.parameter.test(node.parameter)) { 26 | priority += 1100; 27 | priority += 10000; 28 | } 29 | 30 | // doesn't have `name` and `hasBlock` 31 | if ( 32 | !pattern.hasOwnProperty('hasBlock') && 33 | !pattern.hasOwnProperty('name') && 34 | !pattern.hasOwnProperty('paremeter') 35 | ) { 36 | priority = 1; 37 | } 38 | 39 | // patter has `name` and `hasBlock`, but it doesn't match both properties 40 | if (pattern.hasOwnProperty('hasBlock') && pattern.hasOwnProperty('name') && priority < 20000) { 41 | priority = 0; 42 | } 43 | 44 | // patter has `name` and `parameter`, but it doesn't match both properties 45 | if (pattern.hasOwnProperty('name') && pattern.hasOwnProperty('parameter') && priority < 21100) { 46 | priority = 0; 47 | } 48 | 49 | // patter has `name`, `parameter`, and `hasBlock`, but it doesn't match all properties 50 | if ( 51 | pattern.hasOwnProperty('name') && 52 | pattern.hasOwnProperty('parameter') && 53 | pattern.hasOwnProperty('hasBlock') && 54 | priority < 30000 55 | ) { 56 | priority = 0; 57 | } 58 | 59 | return priority; 60 | } 61 | -------------------------------------------------------------------------------- /rules/order/calcRulePatternPriority.js: -------------------------------------------------------------------------------- 1 | export function calcRulePatternPriority(pattern, node) { 2 | // 0 — it pattern doesn't match 3 | // 1 — pattern without `selector` 4 | // 2 — pattern match `selector` 5 | 6 | let priority = 0; 7 | 8 | // doesn't have `selector` 9 | if (!pattern.hasOwnProperty('selector')) { 10 | priority = 1; 11 | } 12 | 13 | // match `selector` 14 | if (pattern.hasOwnProperty('selector') && pattern.selector.test(node.selector)) { 15 | priority = 2; 16 | } 17 | 18 | return priority; 19 | } 20 | -------------------------------------------------------------------------------- /rules/order/checkNode.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import sortNode from 'postcss-sorting/lib/order/sortNode.js'; 3 | import { checkOrder } from './checkOrder.js'; 4 | import { getOrderData } from './getOrderData.js'; 5 | import { ruleName } from './ruleName.js'; 6 | import { messages } from './messages.js'; 7 | 8 | export function checkNode({ node, orderInfo, primaryOption, result, unspecified }) { 9 | let hasRunFixer = false; 10 | 11 | checkAndReport(fixer); 12 | 13 | function checkAndReport(fix) { 14 | let allNodesData = []; 15 | 16 | node.each(function processEveryNode(child) { 17 | if (child.type === 'comment') { 18 | return; 19 | } 20 | 21 | // Receive node description and expectedPosition 22 | let nodeOrderData = getOrderData(orderInfo, child); 23 | 24 | let nodeData = { 25 | node: child, 26 | description: nodeOrderData.description, 27 | expectedPosition: nodeOrderData.expectedPosition, 28 | }; 29 | 30 | allNodesData.push(nodeData); 31 | 32 | let previousNodeData = allNodesData.at(-2); 33 | 34 | // Skip first node 35 | if (!previousNodeData) { 36 | return; 37 | } 38 | 39 | // Try to find the specified node before the current one, 40 | // or use the previous one 41 | let priorSpecifiedNodeData = previousNodeData; 42 | 43 | if (!previousNodeData.expectedPosition) { 44 | let priorSpecifiedNodeData2 = allNodesData 45 | .slice(0, -1) 46 | .reverse() 47 | .find((node2) => Boolean(node2.expectedPosition)); 48 | 49 | if (priorSpecifiedNodeData2) { 50 | priorSpecifiedNodeData = priorSpecifiedNodeData2; 51 | } 52 | } 53 | 54 | let isCorrectOrder = checkOrder({ 55 | firstNodeData: priorSpecifiedNodeData, 56 | secondNodeData: nodeData, 57 | unspecified, 58 | }); 59 | 60 | if (isCorrectOrder) { 61 | return; 62 | } 63 | 64 | stylelint.utils.report({ 65 | message: messages.expected( 66 | nodeData.description, 67 | priorSpecifiedNodeData.description, 68 | ), 69 | node: child, 70 | result, 71 | ruleName, 72 | fix, 73 | }); 74 | }); 75 | } 76 | 77 | function fixer() { 78 | if (hasRunFixer) { 79 | return; 80 | } 81 | 82 | sortNode(node, primaryOption); 83 | 84 | hasRunFixer = true; 85 | 86 | checkAndReport(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /rules/order/checkOrder.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line consistent-return 2 | export function checkOrder({ firstNodeData, secondNodeData, unspecified }) { 3 | let firstNodeIsSpecified = Boolean(firstNodeData.expectedPosition); 4 | let secondNodeIsSpecified = Boolean(secondNodeData.expectedPosition); 5 | 6 | // If both nodes have their position 7 | if (firstNodeIsSpecified && secondNodeIsSpecified) { 8 | return firstNodeData.expectedPosition <= secondNodeData.expectedPosition; 9 | } 10 | 11 | if (!firstNodeIsSpecified && !secondNodeIsSpecified) { 12 | return true; 13 | } 14 | 15 | if (unspecified === 'ignore' && (!firstNodeIsSpecified || !secondNodeIsSpecified)) { 16 | return true; 17 | } 18 | 19 | if (unspecified === 'top' && !firstNodeIsSpecified) { 20 | return true; 21 | } 22 | 23 | if (unspecified === 'top' && !secondNodeIsSpecified) { 24 | return false; 25 | } 26 | 27 | if (unspecified === 'bottom' && !secondNodeIsSpecified) { 28 | return true; 29 | } 30 | 31 | if (unspecified === 'bottom' && !firstNodeIsSpecified) { 32 | return false; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /rules/order/createOrderInfo.js: -------------------------------------------------------------------------------- 1 | import { getDescription } from './getDescription.js'; 2 | import { isString } from '../../utils/validateType.js'; 3 | 4 | export function createOrderInfo(input) { 5 | let order = {}; 6 | let expectedPosition = 0; 7 | 8 | input.forEach((originalItem) => { 9 | let item = originalItem; 10 | 11 | expectedPosition += 1; 12 | 13 | // Convert 'rules' into extended pattern 14 | if (item === 'rules') { 15 | item = { 16 | type: 'rule', 17 | }; 18 | } 19 | 20 | if (item.type === 'rule') { 21 | // It there are no nodes like that create array for them 22 | if (!order[item.type]) { 23 | order[item.type] = []; 24 | } 25 | 26 | let nodeData = { 27 | expectedPosition, 28 | description: getDescription(item), 29 | }; 30 | 31 | if (item.selector) { 32 | nodeData.selector = item.selector; 33 | 34 | if (isString(item.selector)) { 35 | nodeData.selector = new RegExp(item.selector); 36 | } 37 | } 38 | 39 | order[item.type].push(nodeData); 40 | } 41 | 42 | // Convert 'at-rules' into extended pattern 43 | if (item === 'at-rules') { 44 | item = { 45 | type: 'at-rule', 46 | }; 47 | } 48 | 49 | if (item.type === 'at-rule') { 50 | // It there are no nodes like that create array for them 51 | if (!order[item.type]) { 52 | order[item.type] = []; 53 | } 54 | 55 | let nodeData = { 56 | expectedPosition, 57 | description: getDescription(item), 58 | }; 59 | 60 | if (item.name) { 61 | nodeData.name = item.name; 62 | } 63 | 64 | if (item.parameter) { 65 | nodeData.parameter = item.parameter; 66 | 67 | if (isString(item.parameter)) { 68 | nodeData.parameter = new RegExp(item.parameter); 69 | } 70 | } 71 | 72 | if (item.hasBlock !== undefined) { 73 | nodeData.hasBlock = item.hasBlock; 74 | } 75 | 76 | order[item.type].push(nodeData); 77 | } 78 | 79 | if (isString(item)) { 80 | order[item] = { 81 | expectedPosition, 82 | description: getDescription(item), 83 | }; 84 | } 85 | }); 86 | 87 | return order; 88 | } 89 | -------------------------------------------------------------------------------- /rules/order/getDescription.js: -------------------------------------------------------------------------------- 1 | import { isObject } from '../../utils/validateType.js'; 2 | 3 | export function getDescription(item) { 4 | const descriptions = { 5 | 'custom-properties': 'custom property', 6 | 'dollar-variables': '$-variable', 7 | 'at-variables': '@-variable', 8 | 'less-mixins': 'Less mixin', 9 | declarations: 'declaration', 10 | }; 11 | 12 | if (isObject(item)) { 13 | let text; 14 | 15 | if (item.type === 'at-rule') { 16 | text = 'at-rule'; 17 | 18 | if (item.name) { 19 | text = `@${item.name}`; 20 | } 21 | 22 | if (item.parameter) { 23 | text += ` "${item.parameter}"`; 24 | } 25 | 26 | if (item.hasOwnProperty('hasBlock')) { 27 | if (item.hasBlock) { 28 | text += ' with a block'; 29 | } else { 30 | text = `blockless ${text}`; 31 | } 32 | } 33 | } 34 | 35 | if (item.type === 'rule') { 36 | text = 'rule'; 37 | 38 | if (item.name) { 39 | // Prefer 'name' property for better error messaging 40 | text += ` "${item.name}"`; 41 | } else if (item.selector) { 42 | text += ` with selector matching "${item.selector}"`; 43 | } 44 | } 45 | 46 | return text; 47 | } 48 | 49 | // Return description for keyword patterns 50 | return descriptions[item]; 51 | } 52 | -------------------------------------------------------------------------------- /rules/order/getOrderData.js: -------------------------------------------------------------------------------- 1 | import { calcAtRulePatternPriority } from './calcAtRulePatternPriority.js'; 2 | import { calcRulePatternPriority } from './calcRulePatternPriority.js'; 3 | import { getDescription } from './getDescription.js'; 4 | import { isAtVariable } from '../../utils/isAtVariable.js'; 5 | import { isCustomProperty } from '../../utils/isCustomProperty.js'; 6 | import { isDollarVariable } from '../../utils/isDollarVariable.js'; 7 | import { isLessMixin } from '../../utils/isLessMixin.js'; 8 | import { isStandardSyntaxProperty } from '../../utils/isStandardSyntaxProperty.js'; 9 | 10 | export function getOrderData(orderInfo, node) { 11 | let nodeType; 12 | 13 | if (isAtVariable(node)) { 14 | nodeType = 'at-variables'; 15 | } else if (isLessMixin(node)) { 16 | nodeType = 'less-mixins'; 17 | } else if (node.type === 'decl') { 18 | if (isCustomProperty(node.prop)) { 19 | nodeType = 'custom-properties'; 20 | } else if (isDollarVariable(node.prop)) { 21 | nodeType = 'dollar-variables'; 22 | } else if (isStandardSyntaxProperty(node.prop)) { 23 | nodeType = 'declarations'; 24 | } 25 | } else if (node.type === 'rule') { 26 | nodeType = { 27 | type: 'rule', 28 | selector: node.selector, 29 | }; 30 | 31 | const rules = orderInfo.rule; 32 | 33 | // Looking for most specified pattern, because it can match many patterns 34 | if (rules && rules.length) { 35 | let prioritizedPattern; 36 | let max = 0; 37 | 38 | rules.forEach((pattern) => { 39 | const priority = calcRulePatternPriority(pattern, nodeType); 40 | 41 | if (priority > max) { 42 | max = priority; 43 | prioritizedPattern = pattern; 44 | } 45 | }); 46 | 47 | if (max) { 48 | return prioritizedPattern; 49 | } 50 | } 51 | } else if (node.type === 'atrule') { 52 | nodeType = { 53 | type: 'at-rule', 54 | name: node.name, 55 | hasBlock: false, 56 | }; 57 | 58 | if (node.nodes && node.nodes.length) { 59 | nodeType.hasBlock = true; 60 | } 61 | 62 | if (node.params && node.params.length) { 63 | nodeType.parameter = node.params; 64 | } 65 | 66 | const atRules = orderInfo['at-rule']; 67 | 68 | // Looking for most specified pattern, because it can match many patterns 69 | if (atRules && atRules.length) { 70 | let prioritizedPattern; 71 | let max = 0; 72 | 73 | atRules.forEach((pattern) => { 74 | const priority = calcAtRulePatternPriority(pattern, nodeType); 75 | 76 | if (priority > max) { 77 | max = priority; 78 | prioritizedPattern = pattern; 79 | } 80 | }); 81 | 82 | if (max) { 83 | return prioritizedPattern; 84 | } 85 | } 86 | } 87 | 88 | if (orderInfo[nodeType]) { 89 | return orderInfo[nodeType]; 90 | } 91 | 92 | // Return only description if there no patterns for that node 93 | return { 94 | description: getDescription(nodeType), 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /rules/order/index.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import { getContainingNode } from '../../utils/getContainingNode.js'; 3 | import { isRuleWithNodes } from '../../utils/isRuleWithNodes.js'; 4 | import { checkNode } from './checkNode.js'; 5 | import { createOrderInfo } from './createOrderInfo.js'; 6 | import { validatePrimaryOption } from './validatePrimaryOption.js'; 7 | import { ruleName } from './ruleName.js'; 8 | import { messages } from './messages.js'; 9 | 10 | export function rule(primaryOption, options = {}) { 11 | return function ruleBody(root, result) { 12 | let validOptions = stylelint.utils.validateOptions( 13 | result, 14 | ruleName, 15 | { 16 | actual: primaryOption, 17 | possible: validatePrimaryOption, 18 | }, 19 | { 20 | actual: options, 21 | possible: { 22 | unspecified: ['top', 'bottom', 'ignore'], 23 | }, 24 | optional: true, 25 | }, 26 | ); 27 | 28 | if (!validOptions) { 29 | return; 30 | } 31 | 32 | let unspecified = options.unspecified || 'ignore'; 33 | let orderInfo = createOrderInfo(primaryOption); 34 | 35 | let processedParents = []; 36 | 37 | // Check all rules and at-rules recursively 38 | root.walk(function processRulesAndAtrules(originalNode) { 39 | let node = getContainingNode(originalNode); 40 | 41 | // Avoid warnings duplication, caused by interfering in `root.walk()` algorigthm with `getContainingNode()` 42 | if (processedParents.includes(node)) { 43 | return; 44 | } 45 | 46 | processedParents.push(node); 47 | 48 | if (isRuleWithNodes(node)) { 49 | checkNode({ 50 | node, 51 | orderInfo, 52 | primaryOption, 53 | result, 54 | unspecified, 55 | }); 56 | } 57 | }); 58 | }; 59 | } 60 | 61 | rule.ruleName = ruleName; 62 | rule.messages = messages; 63 | rule.primaryOptionArray = true; 64 | rule.meta = { 65 | fixable: true, 66 | url: 'https://github.com/hudochenkov/stylelint-order/blob/master/rules/order/README.md', 67 | }; 68 | -------------------------------------------------------------------------------- /rules/order/messages.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import { ruleName } from './ruleName.js'; 3 | 4 | export const messages = stylelint.utils.ruleMessages(ruleName, { 5 | expected: (first, second) => `Expected ${first} to come before ${second}`, 6 | }); 7 | -------------------------------------------------------------------------------- /rules/order/ruleName.js: -------------------------------------------------------------------------------- 1 | import { namespace } from '../../utils/namespace.js'; 2 | 3 | export const ruleName = namespace('order'); 4 | -------------------------------------------------------------------------------- /rules/order/tests/validate-options.js: -------------------------------------------------------------------------------- 1 | import { ruleName } from '../ruleName.js'; 2 | 3 | testConfig({ 4 | ruleName, 5 | description: 'valid keywords', 6 | valid: true, 7 | config: [ 8 | 'custom-properties', 9 | 'dollar-variables', 10 | 'at-variables', 11 | 'declarations', 12 | 'rules', 13 | 'at-rules', 14 | 'less-mixins', 15 | ], 16 | }); 17 | 18 | testConfig({ 19 | ruleName, 20 | description: 'valid at-rules variants', 21 | valid: true, 22 | config: [ 23 | { 24 | type: 'at-rule', 25 | name: 'include', 26 | hasBlock: true, 27 | }, 28 | { 29 | type: 'at-rule', 30 | name: 'include', 31 | }, 32 | { 33 | type: 'at-rule', 34 | hasBlock: true, 35 | }, 36 | { 37 | type: 'at-rule', 38 | hasBlock: false, 39 | }, 40 | { 41 | type: 'at-rule', 42 | }, 43 | ], 44 | }); 45 | 46 | testConfig({ 47 | ruleName, 48 | description: 'valid rules variants', 49 | valid: true, 50 | config: [ 51 | { 52 | type: 'rule', 53 | selector: /^&:\w/, 54 | }, 55 | { 56 | type: 'rule', 57 | selector: '^&:\\w', 58 | }, 59 | { 60 | type: 'rule', 61 | selector: /^&::\w/, 62 | name: 'Pseudo', 63 | }, 64 | { 65 | type: 'rule', 66 | }, 67 | ], 68 | }); 69 | 70 | testConfig({ 71 | ruleName, 72 | description: 'valid keyword with at-rule variant (keyword last)', 73 | valid: true, 74 | config: [ 75 | { 76 | type: 'at-rule', 77 | }, 78 | 'declarations', 79 | ], 80 | }); 81 | 82 | // testConfig({ 83 | // ruleName, 84 | // description: 'valid keyword with at-rule variant (keyword first)', 85 | // valid: true, 86 | // failing: true, 87 | // config: [ 88 | // 'declarations', 89 | // { 90 | // type: 'at-rule', 91 | // }, 92 | // ], 93 | // }); 94 | 95 | testConfig({ 96 | ruleName, 97 | description: 'invalid keyword', 98 | valid: false, 99 | config: ['custom-property'], 100 | message: `Invalid option "["custom-property"]" for rule "${ruleName}"`, 101 | }); 102 | 103 | testConfig({ 104 | ruleName, 105 | description: 'invalid at-rule type', 106 | valid: false, 107 | config: [ 108 | { 109 | type: 'atrule', 110 | }, 111 | ], 112 | message: `Invalid option "[{"type":"atrule"}]" for rule "${ruleName}"`, 113 | }); 114 | 115 | testConfig({ 116 | ruleName, 117 | description: 'invalid hasBlock property', 118 | valid: false, 119 | config: [ 120 | { 121 | type: 'at-rule', 122 | hasBlock: 'yes', 123 | }, 124 | ], 125 | message: `Invalid option "[{"type":"at-rule","hasBlock":"yes"}]" for rule "${ruleName}"`, 126 | }); 127 | 128 | testConfig({ 129 | ruleName, 130 | description: 'invalid name property', 131 | valid: false, 132 | config: [ 133 | { 134 | type: 'at-rule', 135 | name: '', 136 | }, 137 | ], 138 | message: `Invalid option "[{"type":"at-rule","name":""}]" for rule "${ruleName}"`, 139 | }); 140 | 141 | testConfig({ 142 | ruleName, 143 | description: 'invalid name property with hasBlock defined', 144 | valid: false, 145 | config: [ 146 | { 147 | type: 'at-rule', 148 | hasBlock: true, 149 | name: '', 150 | }, 151 | ], 152 | message: `Invalid option "[{"type":"at-rule","hasBlock":true,"name":""}]" for rule "${ruleName}"`, 153 | }); 154 | 155 | testConfig({ 156 | ruleName, 157 | description: 'valid parameter (string) and name', 158 | valid: true, 159 | config: [ 160 | { 161 | type: 'at-rule', 162 | name: 'include', 163 | parameter: 'media', 164 | }, 165 | ], 166 | }); 167 | 168 | testConfig({ 169 | ruleName, 170 | description: 'valid parameter (RegExp) and name', 171 | valid: true, 172 | config: [ 173 | { 174 | type: 'at-rule', 175 | name: 'include', 176 | parameter: /$media/, 177 | }, 178 | ], 179 | }); 180 | 181 | testConfig({ 182 | ruleName, 183 | description: 'invalid. parameter is empty', 184 | valid: false, 185 | config: [ 186 | { 187 | type: 'at-rule', 188 | name: 'include', 189 | parameter: '', 190 | }, 191 | ], 192 | message: `Invalid option "[{"type":"at-rule","name":"include","parameter":""}]" for rule "${ruleName}"`, 193 | }); 194 | 195 | testConfig({ 196 | ruleName, 197 | description: 'invalid. parameter is not a string', 198 | valid: false, 199 | config: [ 200 | { 201 | type: 'at-rule', 202 | name: 'include', 203 | parameter: null, 204 | }, 205 | ], 206 | message: `Invalid option "[{"type":"at-rule","name":"include","parameter":null}]" for rule "${ruleName}"`, 207 | }); 208 | 209 | testConfig({ 210 | ruleName, 211 | description: 'invalid. parameter without "name" property', 212 | valid: false, 213 | config: [ 214 | { 215 | type: 'at-rule', 216 | parameter: 'media', 217 | }, 218 | ], 219 | message: `Invalid option "[{"type":"at-rule","parameter":"media"}]" for rule "${ruleName}"`, 220 | }); 221 | 222 | testConfig({ 223 | ruleName, 224 | description: 'valid selector (string)', 225 | valid: true, 226 | config: [ 227 | { 228 | type: 'rule', 229 | selector: '^&:hover', 230 | }, 231 | ], 232 | }); 233 | 234 | testConfig({ 235 | ruleName, 236 | description: 'valid selector (RegExp)', 237 | valid: true, 238 | config: [ 239 | { 240 | type: 'rule', 241 | selector: /^&:\w/, 242 | }, 243 | ], 244 | }); 245 | 246 | testConfig({ 247 | ruleName, 248 | description: 'invalid. selector is empty', 249 | valid: false, 250 | config: [ 251 | { 252 | type: 'rule', 253 | selector: '', 254 | }, 255 | ], 256 | message: `Invalid option "[{"type":"rule","selector":""}]" for rule "${ruleName}"`, 257 | }); 258 | 259 | testConfig({ 260 | ruleName, 261 | description: 'invalid. selector is not a string', 262 | valid: false, 263 | config: [ 264 | { 265 | type: 'rule', 266 | selector: null, 267 | }, 268 | ], 269 | message: `Invalid option "[{"type":"rule","selector":null}]" for rule "${ruleName}"`, 270 | }); 271 | 272 | testConfig({ 273 | ruleName, 274 | description: 'invalid. name is empty', 275 | valid: false, 276 | config: [ 277 | { 278 | type: 'rule', 279 | name: '', 280 | }, 281 | ], 282 | message: `Invalid option "[{"type":"rule","name":""}]" for rule "${ruleName}"`, 283 | }); 284 | 285 | testConfig({ 286 | ruleName, 287 | description: 'invalid. name is not a string', 288 | valid: false, 289 | config: [ 290 | { 291 | type: 'rule', 292 | name: null, 293 | }, 294 | ], 295 | message: `Invalid option "[{"type":"rule","name":null}]" for rule "${ruleName}"`, 296 | }); 297 | 298 | testConfig({ 299 | ruleName, 300 | description: 'invalid. selector is valid, but name is invalid', 301 | valid: false, 302 | config: [ 303 | { 304 | type: 'rule', 305 | selector: '^&:hover', 306 | name: null, 307 | }, 308 | ], 309 | message: `Invalid option "[{"type":"rule","selector":"^&:hover","name":null}]" for rule "${ruleName}"`, 310 | }); 311 | 312 | testConfig({ 313 | ruleName, 314 | description: 'invalid. name is valid, but select is invalid', 315 | valid: false, 316 | config: [ 317 | { 318 | type: 'rule', 319 | selector: null, 320 | name: 'Element', 321 | }, 322 | ], 323 | message: `Invalid option "[{"type":"rule","selector":null,"name":"Element"}]" for rule "${ruleName}"`, 324 | }); 325 | -------------------------------------------------------------------------------- /rules/order/validatePrimaryOption.js: -------------------------------------------------------------------------------- 1 | import { isObject, isString } from '../../utils/validateType.js'; 2 | 3 | export function validatePrimaryOption(actualOptions) { 4 | // Otherwise, begin checking array options 5 | if (!Array.isArray(actualOptions)) { 6 | return false; 7 | } 8 | 9 | // Every item in the array must be a certain string or an object 10 | // with a "type" property 11 | if ( 12 | !actualOptions.every((item) => { 13 | if (isString(item)) { 14 | return [ 15 | 'custom-properties', 16 | 'dollar-variables', 17 | 'at-variables', 18 | 'declarations', 19 | 'rules', 20 | 'at-rules', 21 | 'less-mixins', 22 | ].includes(item); 23 | } 24 | 25 | return isObject(item) && item.type !== undefined; 26 | }) 27 | ) { 28 | return false; 29 | } 30 | 31 | const objectItems = actualOptions.filter(isObject); 32 | 33 | if ( 34 | !objectItems.every((item) => { 35 | let result = true; 36 | 37 | if (item.type !== 'at-rule' && item.type !== 'rule') { 38 | return false; 39 | } 40 | 41 | if (item.type === 'at-rule') { 42 | // if parameter is specified, name should be specified also 43 | if (item.parameter !== undefined && item.name === undefined) { 44 | return false; 45 | } 46 | 47 | if (item.hasBlock !== undefined) { 48 | result = item.hasBlock === true || item.hasBlock === false; 49 | } 50 | 51 | if (item.name !== undefined) { 52 | result = isString(item.name) && item.name.length; 53 | } 54 | 55 | if (item.parameter !== undefined) { 56 | result = 57 | (isString(item.parameter) && item.parameter.length) || 58 | isRegExp(item.parameter); 59 | } 60 | } 61 | 62 | if (item.type === 'rule') { 63 | if (item.selector !== undefined) { 64 | result = 65 | (isString(item.selector) && item.selector.length) || 66 | isRegExp(item.selector); 67 | } 68 | 69 | if (result && item.name !== undefined) { 70 | result = isString(item.name) && item.name.length; 71 | } 72 | } 73 | 74 | return result; 75 | }) 76 | ) { 77 | return false; 78 | } 79 | 80 | return true; 81 | } 82 | 83 | function isRegExp(value) { 84 | return Object.prototype.toString.call(value) === '[object RegExp]'; 85 | } 86 | -------------------------------------------------------------------------------- /rules/properties-alphabetical-order/README.md: -------------------------------------------------------------------------------- 1 | # properties-alphabetical-order 2 | 3 | Specify the alphabetical order of properties within declaration blocks. 4 | 5 | ```css 6 | a { 7 | color: pink; 8 | top: 0; 9 | } 10 | /** ↑ 11 | * These properties */ 12 | ``` 13 | 14 | Shorthand properties *must always* precede their longhand counterparts, even if that means they are not alphabetized. 15 | (See also [`declaration-block-no-shorthand-property-overrides`](https://stylelint.io/user-guide/rules/declaration-block-no-shorthand-property-overrides/).) 16 | 17 | Prefixed properties *must always* precede the unprefixed version. 18 | 19 | This rule ignores variables (`$sass`, `@less`, `--custom-property`). 20 | 21 | ## Options 22 | 23 | ### Primary option 24 | 25 | Value type: `boolean`.
26 | Default value: none. 27 | 28 | ```json 29 | { "order/properties-alphabetical-order": true } 30 | ``` 31 | 32 | The following patterns are considered warnings: 33 | 34 | ```css 35 | a { 36 | top: 0; 37 | color: pink; 38 | } 39 | ``` 40 | 41 | ```css 42 | a { 43 | border-bottom-color: pink; 44 | border-color: transparent; 45 | } 46 | ``` 47 | 48 | ```css 49 | a { 50 | -moz-transform: scale(1); 51 | transform: scale(1); 52 | -webkit-transform: scale(1); 53 | } 54 | ``` 55 | 56 | The following patterns are *not* considered warnings: 57 | 58 | ```css 59 | a { 60 | color: pink; 61 | top: 0; 62 | } 63 | ``` 64 | 65 | ```css 66 | a { 67 | border-color: transparent; 68 | border-bottom-color: pink; 69 | } 70 | ``` 71 | 72 | ```css 73 | a { 74 | -webkit-transform: scale(1); 75 | -moz-transform: scale(1); 76 | transform: scale(1); 77 | } 78 | ``` 79 | 80 | ```css 81 | a { 82 | -moz-transform: scale(1); 83 | -webkit-transform: scale(1); 84 | transform: scale(1); 85 | } 86 | ``` 87 | -------------------------------------------------------------------------------- /rules/properties-alphabetical-order/checkChild.js: -------------------------------------------------------------------------------- 1 | import { isStandardSyntaxProperty } from '../../utils/isStandardSyntaxProperty.js'; 2 | import { isCustomProperty } from '../../utils/isCustomProperty.js'; 3 | import * as vendor from '../../utils/vendor.js'; 4 | import { checkAlphabeticalOrder } from '../../utils/checkAlphabeticalOrder.js'; 5 | 6 | export function checkChild(child, allPropData) { 7 | if (child.type !== 'decl') { 8 | return null; 9 | } 10 | 11 | let { prop } = child; 12 | 13 | if (!isStandardSyntaxProperty(prop)) { 14 | return null; 15 | } 16 | 17 | if (isCustomProperty(prop)) { 18 | return null; 19 | } 20 | 21 | let unprefixedPropName = vendor.unprefixed(prop); 22 | 23 | // Hack to allow -moz-osx-font-smoothing to be understood 24 | // just like -webkit-font-smoothing 25 | if (unprefixedPropName.startsWith('osx-')) { 26 | unprefixedPropName = unprefixedPropName.slice(4); 27 | } 28 | 29 | let propData = { 30 | name: prop, 31 | unprefixedName: unprefixedPropName, 32 | index: allPropData.length, 33 | node: child, 34 | }; 35 | 36 | let previousPropData = allPropData.at(-1); 37 | 38 | allPropData.push(propData); 39 | 40 | // Skip first decl 41 | if (!previousPropData) { 42 | return null; 43 | } 44 | 45 | let isCorrectOrder = checkAlphabeticalOrder(previousPropData, propData); 46 | 47 | if (isCorrectOrder) { 48 | return null; 49 | } 50 | 51 | return { 52 | expectedFirst: propData.name, 53 | expectedSecond: previousPropData.name, 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /rules/properties-alphabetical-order/checkNode.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import { checkChild } from './checkChild.js'; 3 | 4 | // eslint-disable-next-line max-params 5 | export function checkNode(node, result, ruleName, messages, fix) { 6 | let allPropData = []; 7 | 8 | node.each(function processEveryNode(child) { 9 | const problem = checkChild(child, allPropData); 10 | 11 | if (problem) { 12 | stylelint.utils.report({ 13 | message: messages.expected(problem.expectedFirst, problem.expectedSecond), 14 | node: child, 15 | result, 16 | ruleName, 17 | fix, 18 | }); 19 | } 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /rules/properties-alphabetical-order/index.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import sortNodeProperties from 'postcss-sorting/lib/properties-order/sortNodeProperties.js'; 3 | import { checkNode } from './checkNode.js'; 4 | import { namespace } from '../../utils/namespace.js'; 5 | import { getContainingNode } from '../../utils/getContainingNode.js'; 6 | import { isRuleWithNodes } from '../../utils/isRuleWithNodes.js'; 7 | 8 | let ruleName = namespace('properties-alphabetical-order'); 9 | 10 | let messages = stylelint.utils.ruleMessages(ruleName, { 11 | expected: (first, second) => `Expected ${first} to come before ${second}`, 12 | }); 13 | 14 | export function rule(actual) { 15 | return function ruleBody(root, result) { 16 | let validOptions = stylelint.utils.validateOptions(result, ruleName, { 17 | actual, 18 | possible: Boolean, 19 | }); 20 | 21 | if (!validOptions) { 22 | return; 23 | } 24 | 25 | let processedParents = []; 26 | 27 | root.walk(function processRulesAndAtrules(input) { 28 | let node = getContainingNode(input); 29 | 30 | // Avoid warnings duplication, caused by interfering in `root.walk()` algorigthm with `getContainingNode()` 31 | if (processedParents.includes(node)) { 32 | return; 33 | } 34 | 35 | processedParents.push(node); 36 | 37 | let hasRunFixer = false; 38 | 39 | function fixer() { 40 | if (hasRunFixer) { 41 | return; 42 | } 43 | 44 | sortNodeProperties(node, { order: 'alphabetical' }); 45 | 46 | hasRunFixer = true; 47 | 48 | checkNode(node, result, ruleName, messages); 49 | } 50 | 51 | if (isRuleWithNodes(node)) { 52 | checkNode(node, result, ruleName, messages, fixer); 53 | } 54 | }); 55 | }; 56 | } 57 | 58 | rule.ruleName = ruleName; 59 | rule.messages = messages; 60 | rule.meta = { 61 | fixable: true, 62 | url: 'https://github.com/hudochenkov/stylelint-order/blob/master/rules/properties-alphabetical-order/README.md', 63 | }; 64 | -------------------------------------------------------------------------------- /rules/properties-alphabetical-order/isOrderCorrect.js: -------------------------------------------------------------------------------- 1 | import { checkChild } from './checkChild.js'; 2 | 3 | export function isOrderCorrect(node) { 4 | const allPropData = []; 5 | 6 | return node.every(function isChildCorrect(child) { 7 | return !checkChild(child, allPropData); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /rules/properties-alphabetical-order/tests/index.js: -------------------------------------------------------------------------------- 1 | import { rule } from '../index.js'; 2 | 3 | const { ruleName, messages } = rule; 4 | 5 | testRule({ 6 | ruleName, 7 | config: [true], 8 | fix: true, 9 | 10 | accept: [ 11 | { 12 | code: 'a { color: pink; }', 13 | }, 14 | { 15 | code: 'a { color: pink; color: red; }', 16 | }, 17 | { 18 | code: 'a { color: pink; color: red; } b { color: pink; color: red; }', 19 | }, 20 | { 21 | code: 'a { color: pink; top: 0; }', 22 | }, 23 | { 24 | code: 'a { border: 1px solid pink; border-left-width: 0; }', 25 | }, 26 | { 27 | code: 'a { color: pink; top: 0; transform: scale(1); }', 28 | }, 29 | { 30 | code: 'a { border-color: transparent; border-bottom-color: pink; }', 31 | }, 32 | { 33 | code: 'a { -moz-transform: scale(1); -webkit-transform: scale(1); transform: scale(1); }', 34 | }, 35 | { 36 | code: 'a { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }', 37 | }, 38 | { 39 | code: 'a { color: pink; -webkit-font-smoothing: antialiased; top: 0; }', 40 | }, 41 | { 42 | code: 'a {{ &:hover { color: red; top: 0; } } }', 43 | }, 44 | { 45 | code: 'a { top: 0; { &:hover { color: red; } } }', 46 | }, 47 | { 48 | code: 'a { top: 0; &:hover { color: red; } }', 49 | }, 50 | { 51 | code: 'a { color: red; width: 0; { &:hover { color: red; top: 0; } } }', 52 | }, 53 | { 54 | code: 'a { color: red; width: 0; &:hover { color: red; top: 0; } }', 55 | }, 56 | { 57 | code: 'a { color: red; width: 0; @media print { color: red; top: 0; } }', 58 | }, 59 | { 60 | code: 'a { $scss: 0; $a: 0; alpha: 0; }', 61 | }, 62 | { 63 | code: 'a { @less: 0; @a: 0; alpha: 0; }', 64 | }, 65 | { 66 | code: 'a { --custom-property: 0; --another: 0; alpha: 0; }', 67 | }, 68 | { 69 | code: 'a { font-size: 1px; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialised; font-weight: bold; }', 70 | }, 71 | { 72 | code: 'a { color: #000; span { bottom: 1em; top: 1em; } width: 25%;}', 73 | }, 74 | { 75 | code: 'a { display: block; margin: 0 auto 5px 0; Margin: 0 auto 5px 0; width: auto; }', 76 | }, 77 | { 78 | code: 'a { display: block; Margin: 0 auto 5px 0; margin: 0 auto 5px 0; width: auto; }', 79 | }, 80 | { 81 | code: 'a { align: center; Border-width: 1px; Border-top-width: 2px; color: red; }', 82 | }, 83 | { 84 | code: 'a { align: center; border-width: 1px; Border-top-width: 2px; color: red; }', 85 | }, 86 | { 87 | code: 'a { align: center; Border-width: 1px; border-top-width: 2px; color: red; }', 88 | }, 89 | ], 90 | 91 | reject: [ 92 | { 93 | code: 'a { top: 0; color: pink; }', 94 | fixed: 'a { color: pink; top: 0; }', 95 | message: messages.expected('color', 'top'), 96 | }, 97 | { 98 | code: 'a { top: 0; color: pink; } b { color: pink; top: 0; }', 99 | fixed: 'a { color: pink; top: 0; } b { color: pink; top: 0; }', 100 | message: messages.expected('color', 'top'), 101 | }, 102 | { 103 | code: 'a { color: pink; transform: scale(1); top: 0; }', 104 | fixed: 'a { color: pink; top: 0; transform: scale(1); }', 105 | message: messages.expected('top', 'transform'), 106 | }, 107 | { 108 | code: 'a { border-bottom-color: pink; border-color: transparent; }', 109 | fixed: 'a { border-color: transparent; border-bottom-color: pink; }', 110 | message: messages.expected('border-color', 'border-bottom-color'), 111 | }, 112 | { 113 | code: 'a { color: pink; top: 0; -moz-transform: scale(1); transform: scale(1); -webkit-transform: scale(1); }', 114 | fixed: 'a { color: pink; top: 0; -moz-transform: scale(1); -webkit-transform: scale(1); transform: scale(1); }', 115 | message: messages.expected('-webkit-transform', 'transform'), 116 | }, 117 | { 118 | code: 'a { color: pink; top: 0; transform: scale(0); -webkit-transform: scale(1); transform: scale(1); }', 119 | fixed: 'a { color: pink; top: 0; -webkit-transform: scale(1); transform: scale(0); transform: scale(1); }', 120 | message: messages.expected('-webkit-transform', 'transform'), 121 | }, 122 | { 123 | code: 'a { -webkit-font-smoothing: antialiased; color: pink; top: 0; }', 124 | fixed: 'a { color: pink; -webkit-font-smoothing: antialiased; top: 0; }', 125 | message: messages.expected('color', '-webkit-font-smoothing'), 126 | }, 127 | { 128 | code: 'a { width: 0; { &:hover { top: 0; color: red; } } }', 129 | fixed: 'a { width: 0; { &:hover { color: red; top: 0; } } }', 130 | message: messages.expected('color', 'top'), 131 | }, 132 | { 133 | code: 'a { &:hover { top: 0; color: red; } }', 134 | fixed: 'a { &:hover { color: red; top: 0; } }', 135 | message: messages.expected('color', 'top'), 136 | }, 137 | { 138 | code: 'a { width: 0; &:hover { top: 0; color: red; } }', 139 | fixed: 'a { width: 0; &:hover { color: red; top: 0; } }', 140 | message: messages.expected('color', 'top'), 141 | }, 142 | { 143 | code: 'a { width: 0; @media print { top: 0; color: red; } }', 144 | fixed: 'a { width: 0; @media print { color: red; top: 0; } }', 145 | message: messages.expected('color', 'top'), 146 | }, 147 | { 148 | code: '@media print { top: 0; color: red; }', 149 | fixed: '@media print { color: red; top: 0; }', 150 | message: messages.expected('color', 'top'), 151 | }, 152 | { 153 | description: 'Fix should apply, when disable comments were used', 154 | code: ` 155 | /* stylelint-disable order/properties-alphabetical-order */ 156 | /* stylelint-enable order/properties-alphabetical-order */ 157 | a { top: 0; color: pink; } 158 | `, 159 | fixed: ` 160 | /* stylelint-disable order/properties-alphabetical-order */ 161 | /* stylelint-enable order/properties-alphabetical-order */ 162 | a { color: pink; top: 0; } 163 | `, 164 | message: messages.expected('color', 'top'), 165 | }, 166 | { 167 | code: 'a { align: center; Border-top-width: 2px; Border-width: 1px; color: red; }', 168 | fixed: 'a { align: center; Border-width: 1px; Border-top-width: 2px; color: red; }', 169 | message: messages.expected('Border-width', 'Border-top-width'), 170 | }, 171 | { 172 | code: 'a { align: center; Border-top-width: 2px; border-width: 1px; color: red; }', 173 | fixed: 'a { align: center; border-width: 1px; Border-top-width: 2px; color: red; }', 174 | message: messages.expected('border-width', 'Border-top-width'), 175 | }, 176 | { 177 | code: 'a { align: center; border-top-width: 2px; Border-width: 1px; color: red; }', 178 | fixed: 'a { align: center; Border-width: 1px; border-top-width: 2px; color: red; }', 179 | message: messages.expected('Border-width', 'border-top-width'), 180 | }, 181 | ], 182 | }); 183 | 184 | testRule({ 185 | ruleName, 186 | config: [true], 187 | customSyntax: 'postcss-styled-syntax', 188 | fix: true, 189 | 190 | accept: [ 191 | { 192 | code: ` 193 | const Component = styled.div\` 194 | color: tomato; 195 | top: 0; 196 | \`; 197 | `, 198 | }, 199 | { 200 | code: ` 201 | const Component = styled.div\` 202 | color: tomato; 203 | \${props => props.great && 'color: red;'} 204 | top: 0; 205 | \`; 206 | `, 207 | }, 208 | { 209 | code: ` 210 | const Component = styled.div\` 211 | color: tomato; 212 | \${props => props.great && 'color: red;'} 213 | top: 0; 214 | 215 | a { 216 | color: tomato; 217 | top: 0; 218 | } 219 | \`; 220 | `, 221 | }, 222 | { 223 | code: ` 224 | const Component = styled.div\` 225 | color: tomato; 226 | top: 0; 227 | 228 | a { 229 | color: tomato; 230 | \${props => props.great && 'color: red;'} 231 | top: 0; 232 | } 233 | \`; 234 | `, 235 | }, 236 | ], 237 | 238 | reject: [ 239 | { 240 | code: ` 241 | const Component = styled.div\` 242 | top: 0; 243 | color: tomato; 244 | \`; 245 | `, 246 | fixed: ` 247 | const Component = styled.div\` 248 | color: tomato; 249 | top: 0; 250 | \`; 251 | `, 252 | message: messages.expected('color', 'top'), 253 | }, 254 | { 255 | code: ` 256 | const Component = styled.div\` 257 | top: 0; 258 | \${props => props.great && 'color: red;'} 259 | color: tomato; 260 | \`; 261 | `, 262 | unfixable: true, 263 | message: messages.expected('color', 'top'), 264 | }, 265 | { 266 | code: ` 267 | const Component = styled.div\` 268 | top: 0; 269 | \${props => props.great && 'color: red;'} 270 | color: tomato; 271 | 272 | a { 273 | color: tomato; 274 | top: 0; 275 | } 276 | \`; 277 | `, 278 | unfixable: true, 279 | message: messages.expected('color', 'top'), 280 | }, 281 | { 282 | code: ` 283 | const Component = styled.div\` 284 | color: tomato; 285 | top: 0; 286 | 287 | a { 288 | top: 0; 289 | \${props => props.great && 'color: red;'} 290 | color: tomato; 291 | } 292 | \`; 293 | `, 294 | unfixable: true, 295 | message: messages.expected('color', 'top'), 296 | }, 297 | ], 298 | }); 299 | -------------------------------------------------------------------------------- /rules/properties-order/README.md: -------------------------------------------------------------------------------- 1 | # properties-order 2 | 3 | Specify the order of properties within declaration blocks. 4 | 5 | ```css 6 | a { 7 | color: pink; 8 | top: 0; 9 | } 10 | /** ↑ 11 | * These properties */ 12 | ``` 13 | 14 | Prefixed properties *must always* precede the unprefixed version. 15 | 16 | This rule ignores variables (`$sass`, `@less`, `--custom-property`). 17 | 18 | * Options 19 | * Optional secondary options 20 | * [`unspecified`](#unspecified) 21 | * [`emptyLineBeforeUnspecified`](#emptyLineBeforeUnspecified) 22 | * [`emptyLineMinimumPropertyThreshold`](#emptyLineMinimumPropertyThreshold) 23 | * [Autofixing caveats](#autofixing-caveats) 24 | 25 | ## Options 26 | 27 | ```ts 28 | type PrimaryOption = Array; 29 | 30 | type Group = { 31 | properties: Array; 32 | emptyLineBefore?: 'always' | 'never' | 'threshold'; 33 | noEmptyLineBetween?: boolean; 34 | groupName?: string; 35 | order?: 'flexible'; 36 | }; 37 | ``` 38 | 39 | Array of unprefixed property names or group objects. Within an order array, you can include: 40 | 41 | * unprefixed property names 42 | * group objects with these properties: 43 | * `properties` (array of strings): The properties in this group. 44 | * `emptyLineBefore: "always" | "never" | "threshold"`: If `always`, this group must be separated from other properties by an empty newline. If emptyLineBefore is `never`, the group must have no empty lines separating it from other properties. By default this property isn't set. 45 | 46 | Rule will check empty lines between properties _only_. However, shared-line comments ignored by rule. Shared-line comment is a comment on the same line as declaration before this comment. 47 | 48 | If `emptyLineBefore` specified, regardless of it's value, the first property in a rule would be forced to not have an empty line before it. 49 | 50 | For `threshold`, refer to the [`emptyLineMinimumPropertyThreshold` documentation](#emptyLineMinimumPropertyThreshold). 51 | 52 | If this option is not working as expected, make sure you don't have `declaration-empty-line-before` configured in a conflicting way in your Stylelint config or config you're extending (e. g. [`stylelint-config-standard`](https://github.com/stylelint/stylelint-config-standard)). 53 | 54 | 55 | * `noEmptyLineBetween`: If `true`, properties within group should not have empty lines between them. 56 | * `groupName`: An optional name for the group. This will be used in error messages. 57 | * `order: "flexible"`: If property isn't set (the default), the properties in this group must come in the order specified. If `"flexible"`, the properties can be in any order as long as they are grouped correctly. 58 | 59 | There are some important details to keep in mind: 60 | 61 | **By default, unlisted properties will be ignored.** So if you specify an array and do not include `display`, that means that the `display` property can be included before or after any other property. *This can be changed with the `unspecified` option* (see below). 62 | 63 | Given: 64 | 65 | ```json 66 | { 67 | "order/properties-order": [ 68 | "transform", 69 | "top", 70 | "color" 71 | ] 72 | } 73 | ``` 74 | 75 | The following patterns are considered warnings: 76 | 77 | ```css 78 | a { 79 | color: pink; 80 | top: 0; 81 | } 82 | ``` 83 | 84 | ```css 85 | a { 86 | -moz-transform: scale(1); 87 | transform: scale(1); 88 | -webkit-transform: scale(1); 89 | } 90 | ``` 91 | 92 | The following patterns are *not* considered warnings: 93 | 94 | ```css 95 | a { 96 | top: 0; 97 | color: pink; 98 | } 99 | ``` 100 | 101 | ```css 102 | a { 103 | -moz-transform: scale(1); 104 | -webkit-transform: scale(1); 105 | transform: scale(1); 106 | } 107 | ``` 108 | 109 | ```css 110 | a { 111 | -webkit-transform: scale(1); 112 | -moz-transform: scale(1); 113 | transform: scale(1); 114 | } 115 | ``` 116 | 117 | Given: 118 | 119 | ```json 120 | { 121 | "order/properties-order": [ 122 | "padding", 123 | "color", 124 | "padding-top" 125 | ] 126 | } 127 | ``` 128 | 129 | The following patterns are considered warnings: 130 | 131 | ```css 132 | a { 133 | color: pink; 134 | padding: 1em; 135 | } 136 | ``` 137 | 138 | ```css 139 | a { 140 | padding-top: 1em; 141 | color: pink; 142 | } 143 | ``` 144 | 145 | The following patterns are *not* considered warnings: 146 | 147 | ```css 148 | a { 149 | padding: 1em; 150 | color: pink; 151 | } 152 | ``` 153 | 154 | ```css 155 | a { 156 | color: pink; 157 | padding-top: 1em; 158 | } 159 | ``` 160 | 161 | Given: 162 | 163 | ```json 164 | { 165 | "order/properties-order": [ 166 | "font-smoothing", 167 | "color" 168 | ] 169 | } 170 | ``` 171 | 172 | Where `font-smoothing` is the unprefixed version of proprietary browser property `-webkit-font-smoothing`. 173 | 174 | The following patterns are considered warnings: 175 | 176 | ```css 177 | a { 178 | color: pink; 179 | -webkit-font-smoothing: antialiased; 180 | } 181 | ``` 182 | 183 | ```css 184 | a { 185 | color: pink; 186 | font-smoothing: antialiased; 187 | } 188 | ``` 189 | 190 | The following patterns are *not* considered warnings: 191 | 192 | ```css 193 | a { 194 | -webkit-font-smoothing: antialiased; 195 | color: pink; 196 | } 197 | ``` 198 | 199 | ```css 200 | a { 201 | font-smoothing: antialiased; 202 | color: pink; 203 | } 204 | ``` 205 | 206 | Given: 207 | 208 | ```json 209 | { 210 | "order/properties-order": [ 211 | "padding", 212 | "padding-top", 213 | "padding-right", 214 | "padding-bottom", 215 | "padding-left", 216 | "color" 217 | ] 218 | } 219 | ``` 220 | 221 | The following patterns are considered warnings: 222 | 223 | ```css 224 | a { 225 | padding-left: 2em; 226 | padding-top: 1em; 227 | padding: 1em; 228 | color: pink; 229 | } 230 | ``` 231 | 232 | The following patterns are *not* considered warnings: 233 | 234 | ```css 235 | a { 236 | padding-top: 1em; 237 | padding-right: 1em; 238 | padding-bottom: 0.5em; 239 | padding-left: 0.5em; 240 | color: pink; 241 | } 242 | ``` 243 | 244 | ```css 245 | a { 246 | padding: 1em; 247 | padding-right: 2em; 248 | padding-left: 2.5em; 249 | color: pink; 250 | } 251 | ``` 252 | 253 | Given: 254 | 255 | ```json 256 | { 257 | "order/properties-order": [ 258 | { 259 | "groupName": "dimensions", 260 | "emptyLineBefore": "always", 261 | "properties": [ 262 | "height", 263 | "width" 264 | ] 265 | }, 266 | { 267 | "groupName": "font", 268 | "emptyLineBefore": "always", 269 | "properties": [ 270 | "font-size", 271 | "font-weight" 272 | ] 273 | } 274 | ] 275 | } 276 | ``` 277 | 278 | The following patterns are considered warnings: 279 | 280 | ```css 281 | a { 282 | height: 1px; 283 | width: 2px; 284 | font-size: 2px; 285 | font-weight: bold; 286 | } 287 | ``` 288 | 289 | ```css 290 | a { 291 | height: 1px; 292 | width: 2px; 293 | 294 | font-weight: bold; 295 | font-size: 2px; 296 | } 297 | ``` 298 | 299 | ```css 300 | a { 301 | width: 2px; 302 | 303 | font-size: 2px; 304 | font-weight: bold; 305 | height: 1px; 306 | } 307 | ``` 308 | 309 | The following patterns are *not* considered warnings: 310 | 311 | ```css 312 | a { 313 | height: 1px; 314 | width: 2px; 315 | 316 | font-size: 2px; 317 | font-weight: bold; 318 | } 319 | ``` 320 | 321 | Given: 322 | 323 | ```json 324 | { 325 | "order/properties-order": [ 326 | { 327 | "emptyLineBefore": "never", 328 | "properties": [ 329 | "height", 330 | "width" 331 | ] 332 | }, 333 | { 334 | "emptyLineBefore": "never", 335 | "properties": [ 336 | "font-size", 337 | "font-weight" 338 | ] 339 | } 340 | ] 341 | } 342 | ``` 343 | 344 | The following patterns are considered warnings: 345 | 346 | ```css 347 | a { 348 | height: 1px; 349 | width: 2px; 350 | 351 | font-size: 2px; 352 | font-weight: bold; 353 | } 354 | ``` 355 | 356 | ```css 357 | a { 358 | height: 1px; 359 | width: 2px; 360 | 361 | font-weight: bold; 362 | font-size: 2px; 363 | } 364 | ``` 365 | 366 | ```css 367 | a { 368 | width: 2px; 369 | 370 | font-size: 2px; 371 | font-weight: bold; 372 | height: 1px; 373 | } 374 | ``` 375 | 376 | The following patterns are *not* considered warnings: 377 | 378 | ```css 379 | a { 380 | height: 1px; 381 | width: 2px; 382 | font-size: 2px; 383 | font-weight: bold; 384 | } 385 | ``` 386 | 387 | Given: 388 | 389 | ```json 390 | { 391 | "order/properties-order": [ 392 | { 393 | "emptyLineBefore": "always", 394 | "noEmptyLineBetween": true, 395 | "properties": [ 396 | "height", 397 | "width" 398 | ] 399 | }, 400 | { 401 | "emptyLineBefore": "always", 402 | "noEmptyLineBetween": true, 403 | "properties": [ 404 | "font-size", 405 | "font-weight" 406 | ] 407 | } 408 | ] 409 | } 410 | ``` 411 | 412 | The following pattern is considered warnings: 413 | 414 | ```css 415 | a { 416 | height: 1px; 417 | 418 | width: 2px; 419 | 420 | font-size: 2px; 421 | 422 | font-weight: bold; 423 | } 424 | ``` 425 | 426 | The following patterns is *not* considered warnings: 427 | 428 | ```css 429 | a { 430 | height: 1px; 431 | width: 2px; 432 | 433 | font-size: 2px; 434 | font-weight: bold; 435 | } 436 | ``` 437 | 438 | Given: 439 | 440 | ```json 441 | { 442 | "order/properties-order": [ 443 | "height", 444 | "width", 445 | { 446 | "order": "flexible", 447 | "properties": [ 448 | "color", 449 | "font-size", 450 | "font-weight" 451 | ] 452 | } 453 | ] 454 | } 455 | ``` 456 | 457 | The following patterns are considered warnings: 458 | 459 | ```css 460 | a { 461 | height: 1px; 462 | font-weight: bold; 463 | width: 2px; 464 | } 465 | ``` 466 | 467 | ```css 468 | a { 469 | width: 2px; 470 | height: 1px; 471 | font-weight: bold; 472 | } 473 | ``` 474 | 475 | ```css 476 | a { 477 | height: 1px; 478 | color: pink; 479 | width: 2px; 480 | font-weight: bold; 481 | } 482 | ``` 483 | 484 | The following patterns are *not* considered warnings: 485 | 486 | ```css 487 | a { 488 | height: 1px; 489 | width: 2px; 490 | color: pink; 491 | font-size: 2px; 492 | font-weight: bold; 493 | } 494 | ``` 495 | 496 | ```css 497 | a { 498 | height: 1px; 499 | width: 2px; 500 | font-size: 2px; 501 | color: pink; 502 | font-weight: bold; 503 | } 504 | ``` 505 | 506 | ## Optional secondary options 507 | 508 | ```ts 509 | type SecondaryOptions = { 510 | unspecified?: "top" | "bottom" | "bottomAlphabetical" | "ignore"; 511 | emptyLineBeforeUnspecified?: "always" | "never" | "threshold"; 512 | emptyLineMinimumPropertyThreshold?: number; 513 | }; 514 | ``` 515 | 516 | ### `unspecified` 517 | 518 | Value type: `"top" | "bottom" | "bottomAlphabetical" | "ignore"`.
519 | Default value: `"ignore"`. 520 | 521 | These options only apply if you've defined your own array of properties. 522 | 523 | Default behavior is the same as `"ignore"`: an unspecified property can appear before or after any other property. 524 | 525 | With `"top"`, unspecified properties are expected *before* any specified properties. With `"bottom"`, unspecified properties are expected *after* any specified properties. With `"bottomAlphabetical"`, unspecified properties are expected *after* any specified properties, and the unspecified properties are expected to be in alphabetical order. (See [properties-alphabetical-order](../properties-alphabetical-order/README.md) more more details on the alphabetization rules.) 526 | 527 | Given: 528 | 529 | ```json 530 | { 531 | "order/properties-order": [ 532 | ["color", "background"], 533 | { "unspecified": "ignore" } 534 | ] 535 | } 536 | ``` 537 | 538 | The following patterns are *not* considered warnings: 539 | 540 | ```css 541 | a { 542 | color: pink; 543 | background: orange; 544 | left: 0; 545 | } 546 | ``` 547 | 548 | ```css 549 | a { 550 | left: 0; 551 | color: pink; 552 | background: orange; 553 | } 554 | ``` 555 | 556 | ```css 557 | a { 558 | color: pink; 559 | left: 0; 560 | background: orange; 561 | } 562 | ``` 563 | 564 | Given: 565 | 566 | ```json 567 | { 568 | "order/properties-order": [ 569 | ["color", "background"], 570 | { "unspecified": "top" } 571 | ] 572 | } 573 | ``` 574 | 575 | The following patterns are considered warnings: 576 | 577 | ```css 578 | a { 579 | color: pink; 580 | background: orange; 581 | left: 0; 582 | } 583 | ``` 584 | 585 | ```css 586 | a { 587 | color: pink; 588 | left: 0; 589 | background: orange; 590 | } 591 | ``` 592 | 593 | The following patterns are *not* considered warnings: 594 | 595 | ```css 596 | a { 597 | left: 0; 598 | color: pink; 599 | background: orange; 600 | } 601 | ``` 602 | 603 | Given: 604 | 605 | ```json 606 | { 607 | "order/properties-order": [ 608 | ["color", "background"], 609 | { "unspecified": "bottom" } 610 | ] 611 | } 612 | ``` 613 | 614 | The following patterns are considered warnings: 615 | 616 | ```css 617 | a { 618 | left: 0; 619 | color: pink; 620 | background: orange; 621 | } 622 | ``` 623 | 624 | ```css 625 | a { 626 | color: pink; 627 | left: 0; 628 | background: orange; 629 | } 630 | ``` 631 | 632 | The following patterns are *not* considered warnings: 633 | 634 | ```css 635 | a { 636 | color: pink; 637 | background: orange; 638 | left: 0; 639 | } 640 | ``` 641 | 642 | Given: 643 | 644 | ```json 645 | { 646 | "order/properties-order": [ 647 | ["composes"], 648 | { "unspecified": "bottomAlphabetical" } 649 | ] 650 | } 651 | ``` 652 | 653 | The following patterns are considered warnings: 654 | 655 | ```css 656 | a { 657 | align-items: flex-end; 658 | composes: b; 659 | left: 0; 660 | } 661 | ``` 662 | 663 | ```css 664 | a { 665 | composes: b; 666 | left: 0; 667 | align-items: flex-end; 668 | } 669 | ``` 670 | 671 | The following patterns are *not* considered warnings: 672 | 673 | ```css 674 | a { 675 | composes: b; 676 | align-items: flex-end; 677 | left: 0; 678 | } 679 | ``` 680 | 681 | ### `emptyLineBeforeUnspecified` 682 | 683 | Value type: `"always" | "never" | "threshold"`.
684 | Default value: none. 685 | 686 | Default behavior does not enforce the presence of an empty line before an unspecified block of properties (`"ignore"`). 687 | 688 | If `"always"`, the unspecified group must be separated from other properties by an empty newline. 689 | If `"never"`, the unspecified group must have no empty lines separating it from other properties. 690 | 691 | For `"threshold"`, see the [`emptyLineMinimumPropertyThreshold` documentation](#emptyLineMinimumPropertyThreshold) for more information. 692 | 693 | If `emptyLineBeforeUnspecified` specified, regardless of it's value, if the first property in a rule is target of this option, that property would be forced to not have an empty line before it. 694 | 695 | Given: 696 | 697 | ```json 698 | { 699 | "order/properties-order": [ 700 | [ 701 | "height", 702 | "width", 703 | ], 704 | { 705 | "unspecified": "bottom", 706 | "emptyLineBeforeUnspecified": "always" 707 | } 708 | ] 709 | } 710 | ``` 711 | 712 | The following pattern is considered warnings: 713 | 714 | ```css 715 | a { 716 | height: 1px; 717 | width: 2px; 718 | color: blue; 719 | } 720 | ``` 721 | 722 | The following patterns is *not* considered warnings: 723 | 724 | ```css 725 | a { 726 | height: 1px; 727 | width: 2px; 728 | 729 | color: blue; 730 | } 731 | ``` 732 | 733 | ### `emptyLineMinimumPropertyThreshold` 734 | 735 | Value type: `number`.
736 | Default value: none. 737 | 738 | If a group is configured with `emptyLineBefore: "threshold"`, the empty line behaviour toggles based on the number of properties in the rule. 739 | 740 | When the configured minimum property threshold is reached, empty lines are **inserted**. When the number of properties is **less than** the minimum property threshold, empty lines are **removed**. 741 | 742 | _e.g. threshold set to **3**, and there are **5** properties in total, then groups set to `"threshold"` will have an empty line inserted._ 743 | 744 | The same behaviour is applied to unspecified groups when `emptyLineBeforeUnspecified: "threshold"` 745 | 746 | Given: 747 | 748 | ```json 749 | { 750 | "order/properties-order": [ 751 | [ 752 | { 753 | "emptyLineBefore": "threshold", 754 | "properties": ["display"] 755 | }, 756 | { 757 | "emptyLineBefore": "threshold", 758 | "properties": ["height", "width"] 759 | }, 760 | { 761 | "emptyLineBefore": "always", 762 | "properties": ["border"] 763 | }, 764 | { 765 | "emptyLineBefore": "never", 766 | "properties": ["transform"] 767 | }, 768 | ], 769 | { 770 | "unspecified": "bottom", 771 | "emptyLineBeforeUnspecified": "threshold", 772 | "emptyLineMinimumPropertyThreshold": 4 773 | } 774 | ] 775 | } 776 | ``` 777 | 778 | The following patterns are considered warnings: 779 | 780 | ```css 781 | a { 782 | display: block; 783 | 784 | height: 1px; 785 | width: 2px; 786 | color: blue; 787 | } 788 | 789 | a { 790 | display: block; 791 | 792 | height: 1px; 793 | width: 2px; 794 | border: 0; 795 | color: blue; 796 | } 797 | 798 | a { 799 | display: block; 800 | 801 | height: 1px; 802 | width: 2px; 803 | border: 0; 804 | 805 | transform: none; 806 | color: blue; 807 | } 808 | ``` 809 | 810 | The following patterns are *not* considered warnings: 811 | 812 | ```css 813 | a { 814 | display: block; 815 | height: 1px; 816 | width: 2px; 817 | } 818 | 819 | a { 820 | display: block; 821 | 822 | height: 1px; 823 | width: 2px; 824 | 825 | border: 0; 826 | } 827 | 828 | a { 829 | display: block; 830 | 831 | height: 1px; 832 | width: 2px; 833 | 834 | border: 0; 835 | transform: none; 836 | } 837 | 838 | a { 839 | display: block; 840 | height: 1px; 841 | 842 | border: 0; 843 | } 844 | 845 | a { 846 | border: 0; 847 | transform: none; 848 | color: blue; 849 | } 850 | 851 | a { 852 | display: block; 853 | 854 | height: 1px; 855 | width: 2px; 856 | 857 | border: 0; 858 | transform: none; 859 | 860 | color: blue; 861 | } 862 | ``` 863 | 864 | ## Autofixing caveats 865 | 866 | Properties will be grouped together, if other node types between them (except comments). They will be grouped with the first found property. E.g.: 867 | 868 | ```css 869 | /* Before: */ 870 | a { 871 | --custom-prop: 10px; 872 | top: 0; 873 | --another-custom-prop: 10px; 874 | bottom: 2px; 875 | } 876 | 877 | /* After: */ 878 | a { 879 | --custom-prop: 10px; 880 | top: 0; 881 | bottom: 2px; 882 | --another-custom-prop: 10px; 883 | } 884 | ``` 885 | 886 | If `unspecified` secondary option was set to `ignore`, it will be re-set to `bottom`. 887 | -------------------------------------------------------------------------------- /rules/properties-order/addEmptyLineBefore.js: -------------------------------------------------------------------------------- 1 | // Add an empty line before a node. Mutates the node. 2 | export function addEmptyLineBefore(node, newline) { 3 | if (!/\r?\n/.test(node.raws.before)) { 4 | node.raws.before = newline.repeat(2) + node.raws.before; 5 | } else if (/^\r?\n/.test(node.raws.before)) { 6 | node.raws.before = newline + node.raws.before; 7 | } else if (/\r?\n$/.test(node.raws.before)) { 8 | node.raws.before = node.raws.before + newline; 9 | } else { 10 | node.raws.before = node.raws.before.replace(/(\r?\n)/, `${newline}$1`); 11 | } 12 | 13 | return node; 14 | } 15 | -------------------------------------------------------------------------------- /rules/properties-order/checkEmptyLineBefore.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import { isString } from '../../utils/validateType.js'; 3 | import { addEmptyLineBefore } from './addEmptyLineBefore.js'; 4 | import { hasEmptyLineBefore } from './hasEmptyLineBefore.js'; 5 | import { removeEmptyLinesBefore } from './removeEmptyLinesBefore.js'; 6 | import { ruleName } from './ruleName.js'; 7 | import { messages } from './messages.js'; 8 | 9 | export function checkEmptyLineBefore({ 10 | firstPropData, 11 | secondPropData, 12 | propsCount, 13 | lastKnownSeparatedGroup, 14 | context, 15 | emptyLineBeforeUnspecified, 16 | emptyLineMinimumPropertyThreshold, 17 | primaryOption, 18 | result, 19 | }) { 20 | let firstPropIsSpecified = Boolean(firstPropData.orderData); 21 | let secondPropIsSpecified = Boolean(secondPropData.orderData); 22 | 23 | // Check newlines between groups 24 | let firstPropGroup = firstPropIsSpecified 25 | ? firstPropData.orderData.separatedGroup 26 | : lastKnownSeparatedGroup; 27 | let secondPropGroup = secondPropIsSpecified 28 | ? secondPropData.orderData.separatedGroup 29 | : lastKnownSeparatedGroup; 30 | 31 | let startOfSpecifiedGroup = secondPropIsSpecified && firstPropGroup !== secondPropGroup; 32 | let startOfUnspecifiedGroup = firstPropIsSpecified && !secondPropIsSpecified; 33 | 34 | if (startOfSpecifiedGroup || startOfUnspecifiedGroup) { 35 | // Get an array of just the property groups, remove any solo properties 36 | let groups = primaryOption.filter((item) => !isString(item)); 37 | 38 | let emptyLineBefore = 39 | groups[secondPropGroup - 2] && groups[secondPropGroup - 2].emptyLineBefore; 40 | 41 | if (startOfUnspecifiedGroup) { 42 | emptyLineBefore = emptyLineBeforeUnspecified; 43 | } 44 | 45 | // Threshold logic 46 | let belowEmptyLineThreshold = propsCount < emptyLineMinimumPropertyThreshold; 47 | let emptyLineThresholdInsertLines = 48 | emptyLineBefore === 'threshold' && !belowEmptyLineThreshold; 49 | let emptyLineThresholdRemoveLines = 50 | emptyLineBefore === 'threshold' && belowEmptyLineThreshold; 51 | 52 | if ( 53 | (emptyLineBefore === 'always' || emptyLineThresholdInsertLines) && 54 | !hasEmptyLineBefore(secondPropData.node) 55 | ) { 56 | stylelint.utils.report({ 57 | message: messages.expectedEmptyLineBefore(secondPropData.name), 58 | node: secondPropData.node, 59 | result, 60 | ruleName, 61 | fix: () => { 62 | addEmptyLineBefore(secondPropData.node, context.newline); 63 | }, 64 | }); 65 | } else if ( 66 | (emptyLineBefore === 'never' || emptyLineThresholdRemoveLines) && 67 | hasEmptyLineBefore(secondPropData.node) 68 | ) { 69 | stylelint.utils.report({ 70 | message: messages.rejectedEmptyLineBefore(secondPropData.name), 71 | node: secondPropData.node, 72 | result, 73 | ruleName, 74 | fix: () => { 75 | removeEmptyLinesBefore(secondPropData.node, context.newline); 76 | }, 77 | }); 78 | } 79 | } 80 | 81 | // Check newlines between properties inside a group 82 | if ( 83 | firstPropIsSpecified && 84 | secondPropIsSpecified && 85 | firstPropData.orderData.groupPosition === secondPropData.orderData.groupPosition 86 | ) { 87 | if ( 88 | secondPropData.orderData.noEmptyLineBeforeInsideGroup && 89 | hasEmptyLineBefore(secondPropData.node) 90 | ) { 91 | stylelint.utils.report({ 92 | message: messages.rejectedEmptyLineBefore(secondPropData.name), 93 | node: secondPropData.node, 94 | result, 95 | ruleName, 96 | fix: () => { 97 | removeEmptyLinesBefore(secondPropData.node, context.newline); 98 | }, 99 | }); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /rules/properties-order/checkEmptyLineBeforeFirstProp.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import { isString } from '../../utils/validateType.js'; 3 | import { ruleName } from './ruleName.js'; 4 | import { messages } from './messages.js'; 5 | import { hasEmptyLineBefore } from './hasEmptyLineBefore.js'; 6 | import { removeEmptyLinesBefore } from './removeEmptyLinesBefore.js'; 7 | 8 | export function checkEmptyLineBeforeFirstProp({ 9 | propData, 10 | primaryOption, 11 | emptyLineBeforeUnspecified, 12 | context, 13 | result, 14 | }) { 15 | let emptyLineBefore = false; 16 | 17 | if (propData.orderData) { 18 | // Get an array of just the property groups, remove any solo properties 19 | let groups = primaryOption.filter((item) => !isString(item)); 20 | 21 | emptyLineBefore = 22 | groups[propData.orderData.separatedGroup - 2] && 23 | groups[propData.orderData.separatedGroup - 2].emptyLineBefore; 24 | } else if (emptyLineBeforeUnspecified) { 25 | emptyLineBefore = true; 26 | } 27 | 28 | if (emptyLineBefore && hasEmptyLineBefore(propData.node)) { 29 | stylelint.utils.report({ 30 | message: messages.rejectedEmptyLineBefore(propData.name), 31 | node: propData.node, 32 | result, 33 | ruleName, 34 | fix: () => { 35 | removeEmptyLinesBefore(propData.node, context.newline); 36 | }, 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /rules/properties-order/checkNodeForEmptyLines.js: -------------------------------------------------------------------------------- 1 | import { isProperty } from '../../utils/isProperty.js'; 2 | import { checkEmptyLineBefore } from './checkEmptyLineBefore.js'; 3 | import { checkEmptyLineBeforeFirstProp } from './checkEmptyLineBeforeFirstProp.js'; 4 | import { getNodeData } from './getNodeData.js'; 5 | 6 | export function checkNodeForEmptyLines({ 7 | node, 8 | context, 9 | emptyLineBeforeUnspecified, 10 | emptyLineMinimumPropertyThreshold, 11 | expectedOrder, 12 | primaryOption, 13 | result, 14 | }) { 15 | let lastKnownSeparatedGroup = 1; 16 | 17 | let propsCount = node.nodes.filter((item) => isProperty(item)).length; 18 | let allNodesData = node.nodes.map((child) => getNodeData(child, expectedOrder)); 19 | 20 | allNodesData.forEach(function checkEveryPropForEmptyLine(nodeData, index) { 21 | let previousNodeData = allNodesData[index - 1]; 22 | 23 | // if previous node is shared-line comment, use second previous node 24 | if ( 25 | previousNodeData && 26 | previousNodeData.node.type === 'comment' && 27 | !previousNodeData.node.raw('before').includes('\n') 28 | ) { 29 | previousNodeData = allNodesData[index - 2]; 30 | } 31 | 32 | // skip first decl 33 | if (!previousNodeData) { 34 | return; 35 | } 36 | 37 | // Nodes should be standard declarations 38 | if (!isProperty(previousNodeData.node) || !isProperty(nodeData.node)) { 39 | return; 40 | } 41 | 42 | checkEmptyLineBefore({ 43 | firstPropData: previousNodeData, 44 | secondPropData: nodeData, 45 | propsCount, 46 | lastKnownSeparatedGroup, 47 | context, 48 | emptyLineBeforeUnspecified, 49 | emptyLineMinimumPropertyThreshold, 50 | primaryOption, 51 | result, 52 | }); 53 | }); 54 | 55 | // Check if empty line before first prop should be removed 56 | if (isProperty(allNodesData[0].node)) { 57 | checkEmptyLineBeforeFirstProp({ 58 | propData: allNodesData[0], 59 | primaryOption, 60 | emptyLineBeforeUnspecified, 61 | context, 62 | result, 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /rules/properties-order/checkNodeForOrder.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import sortNodeProperties from 'postcss-sorting/lib/properties-order/sortNodeProperties.js'; 3 | import { isProperty } from '../../utils/isProperty.js'; 4 | import { checkOrder } from './checkOrder.js'; 5 | import { getNodeData } from './getNodeData.js'; 6 | import { createFlatOrder } from './createFlatOrder.js'; 7 | import { ruleName } from './ruleName.js'; 8 | import { messages } from './messages.js'; 9 | 10 | export function checkNodeForOrder({ node, primaryOption, unspecified, result, expectedOrder }) { 11 | /* 12 | Solution is not ideal. 13 | 14 | 1. Stylelint requires to use `fix` function for every report since 16.8.2 https://stylelint.io/user-guide/options/#fix:~:text=When%20a%20rule%20relies%20on%20the%20deprecated%20context.fix%20and%20a%20source%20contains%3A Otherwise it will not run fixer if disabled comments present anywhere in the source. 15 | 16 | 2. PostCSS Sorting sometimes can't fix order, because fixing could break the code. https://github.com/hudochenkov/postcss-sorting?tab=readme-ov-file#caveats 17 | 18 | 3. Fixing an order piece by piece is complicated, because it affects multiple properties. With fixing one by one it is possible that one property if fixed, but then another property, which related to the first one, is fixed, and that introduce a new error with the first property. 19 | 20 | Given all that this is what code does: 21 | 22 | - Check order of properties 23 | - If order is correct, do nothing 24 | - If order is incorrect, run fixer 25 | - Fixer fixes the WHOLE file 26 | - Check order of properties again and report violations WITHOUT fixer 27 | - Because Stylelint runs fixer for every violation, but we already fixed the whole file, skip the fixer for the next violations 28 | */ 29 | let hasRunFixer = false; 30 | 31 | checkAndReport(fixer); 32 | 33 | function checkAndReport(fix) { 34 | const allPropertiesData = node.nodes 35 | .filter((item) => isProperty(item)) 36 | .map((item) => getNodeData(item, expectedOrder)); 37 | 38 | allPropertiesData.forEach((propertyData, index, listOfProperties) => { 39 | // Skip first decl 40 | if (index === 0) { 41 | return; 42 | } 43 | 44 | const previousPropertyData = listOfProperties[index - 1]; 45 | 46 | const checkedOrder = checkOrder({ 47 | firstPropertyData: previousPropertyData, 48 | secondPropertyData: propertyData, 49 | unspecified, 50 | allPropertiesData: listOfProperties.slice(0, index), 51 | }); 52 | 53 | if (!checkedOrder.isCorrect) { 54 | const { orderData } = checkedOrder.secondNode; 55 | 56 | stylelint.utils.report({ 57 | message: messages.expected( 58 | checkedOrder.secondNode.name, 59 | checkedOrder.firstNode.name, 60 | orderData && orderData.groupName, 61 | ), 62 | node: checkedOrder.secondNode.node, 63 | result, 64 | ruleName, 65 | fix, 66 | }); 67 | } 68 | }); 69 | } 70 | 71 | function fixer() { 72 | if (hasRunFixer) { 73 | return; 74 | } 75 | 76 | sortNodeProperties(node, { 77 | order: createFlatOrder(primaryOption), 78 | unspecifiedPropertiesPosition: unspecified === 'ignore' ? 'bottom' : unspecified, 79 | }); 80 | 81 | hasRunFixer = true; 82 | 83 | checkAndReport(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /rules/properties-order/checkOrder.js: -------------------------------------------------------------------------------- 1 | import { checkAlphabeticalOrder } from '../../utils/checkAlphabeticalOrder.js'; 2 | import * as vendor from '../../utils/vendor.js'; 3 | 4 | // eslint-disable-next-line consistent-return 5 | export function checkOrder({ 6 | firstPropertyData, 7 | secondPropertyData, 8 | allPropertiesData, 9 | unspecified, 10 | }) { 11 | function report(isCorrect, firstNode = firstPropertyData, secondNode = secondPropertyData) { 12 | return { 13 | isCorrect, 14 | firstNode, 15 | secondNode, 16 | }; 17 | } 18 | 19 | let firstPropName = firstPropertyData.name.toLowerCase(); 20 | let secondPropName = secondPropertyData.name.toLowerCase(); 21 | let firstPropUnprefixedName = firstPropertyData.unprefixedName.toLowerCase(); 22 | let secondPropUnprefixedName = secondPropertyData.unprefixedName.toLowerCase(); 23 | 24 | if (firstPropUnprefixedName === secondPropUnprefixedName) { 25 | // If first property has no prefix and second property has prefix 26 | if (!vendor.prefix(firstPropName).length && vendor.prefix(secondPropName).length) { 27 | return report(false); 28 | } 29 | 30 | return report(true); 31 | } 32 | 33 | const firstPropIsSpecified = Boolean(firstPropertyData.orderData); 34 | const secondPropIsSpecified = Boolean(secondPropertyData.orderData); 35 | 36 | // Check actual known properties 37 | if (firstPropIsSpecified && secondPropIsSpecified) { 38 | return report( 39 | firstPropertyData.orderData.expectedPosition <= 40 | secondPropertyData.orderData.expectedPosition, 41 | ); 42 | } 43 | 44 | if (!firstPropIsSpecified && secondPropIsSpecified) { 45 | // If first prop is unspecified, look for a specified prop before it to 46 | // compare to the current prop 47 | let priorSpecifiedPropData = allPropertiesData 48 | .slice(0, -1) 49 | .reverse() 50 | .find((declaration) => Boolean(declaration.orderData)); 51 | 52 | if ( 53 | priorSpecifiedPropData && 54 | priorSpecifiedPropData.orderData && 55 | priorSpecifiedPropData.orderData.expectedPosition > 56 | secondPropertyData.orderData.expectedPosition 57 | ) { 58 | return report(false, priorSpecifiedPropData, secondPropertyData); 59 | } 60 | } 61 | 62 | // Now deal with unspecified props 63 | // Starting with bottomAlphabetical as it requires more specific conditionals 64 | if (unspecified === 'bottomAlphabetical' && firstPropIsSpecified && !secondPropIsSpecified) { 65 | return report(true); 66 | } 67 | 68 | if (unspecified === 'bottomAlphabetical' && !firstPropIsSpecified && !secondPropIsSpecified) { 69 | if (checkAlphabeticalOrder(firstPropertyData, secondPropertyData)) { 70 | return report(true); 71 | } 72 | 73 | return report(false); 74 | } 75 | 76 | if (unspecified === 'bottomAlphabetical' && !firstPropIsSpecified) { 77 | return report(false); 78 | } 79 | 80 | if (!firstPropIsSpecified && !secondPropIsSpecified) { 81 | return report(true); 82 | } 83 | 84 | if (unspecified === 'ignore' && (!firstPropIsSpecified || !secondPropIsSpecified)) { 85 | return report(true); 86 | } 87 | 88 | if (unspecified === 'top' && !firstPropIsSpecified) { 89 | return report(true); 90 | } 91 | 92 | if (unspecified === 'top' && !secondPropIsSpecified) { 93 | return report(false); 94 | } 95 | 96 | if (unspecified === 'bottom' && !secondPropIsSpecified) { 97 | return report(true); 98 | } 99 | 100 | if (unspecified === 'bottom' && !firstPropIsSpecified) { 101 | return report(false); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /rules/properties-order/createFlatOrder.js: -------------------------------------------------------------------------------- 1 | import { isString } from '../../utils/validateType.js'; 2 | 3 | export function createFlatOrder(order) { 4 | const flatOrder = []; 5 | 6 | appendGroup(order); 7 | 8 | function appendGroup(items) { 9 | items.forEach((item) => appendItem(item)); 10 | } 11 | 12 | function appendItem(item) { 13 | if (isString(item)) { 14 | flatOrder.push(item); 15 | 16 | return; 17 | } 18 | 19 | appendGroup(item.properties); 20 | } 21 | 22 | return flatOrder; 23 | } 24 | -------------------------------------------------------------------------------- /rules/properties-order/createOrderInfo.js: -------------------------------------------------------------------------------- 1 | import { isString } from '../../utils/validateType.js'; 2 | 3 | export function createOrderInfo(input) { 4 | let order = {}; 5 | let expectedPosition = 0; 6 | let separatedGroup = 1; 7 | let groupPosition = -1; 8 | 9 | appendGroup({ properties: input }); 10 | 11 | function appendGroup(group) { 12 | groupPosition += 1; 13 | group.properties.forEach((item) => appendItem(item, false, group)); 14 | } 15 | 16 | function appendItem(item, inFlexibleGroup, group) { 17 | if (isString(item)) { 18 | // In flexible groups, the expectedPosition does not ascend 19 | // to make that flexibility work; 20 | // otherwise, it will always ascend 21 | if (!inFlexibleGroup) { 22 | expectedPosition += 1; 23 | } 24 | 25 | order[item] = { 26 | separatedGroup, 27 | groupPosition, 28 | expectedPosition, 29 | groupName: group.groupName, 30 | noEmptyLineBeforeInsideGroup: group.noEmptyLineBetween, 31 | }; 32 | 33 | return; 34 | } 35 | 36 | // If item is not a string, it's a group... 37 | if (item.emptyLineBefore) { 38 | separatedGroup += 1; 39 | } 40 | 41 | if (item.order === 'flexible') { 42 | expectedPosition += 1; 43 | groupPosition += 1; 44 | 45 | item.properties.forEach((property) => { 46 | appendItem(property, true, item); 47 | }); 48 | } else { 49 | appendGroup(item); 50 | } 51 | } 52 | 53 | return order; 54 | } 55 | -------------------------------------------------------------------------------- /rules/properties-order/getNodeData.js: -------------------------------------------------------------------------------- 1 | import { isProperty } from '../../utils/isProperty.js'; 2 | import * as vendor from '../../utils/vendor.js'; 3 | 4 | export function getNodeData(node, expectedOrder) { 5 | if (isProperty(node)) { 6 | let { prop } = node; 7 | let unprefixedName = vendor.unprefixed(prop).toLowerCase(); 8 | 9 | // Hack to allow -moz-osx-font-smoothing to be understood 10 | // just like -webkit-font-smoothing 11 | if (unprefixedName.startsWith('osx-')) { 12 | unprefixedName = unprefixedName.slice(4); 13 | } 14 | 15 | return { 16 | node, 17 | name: prop, 18 | unprefixedName, 19 | orderData: expectedOrder[unprefixedName], 20 | }; 21 | } 22 | 23 | return { 24 | node, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /rules/properties-order/hasEmptyLineBefore.js: -------------------------------------------------------------------------------- 1 | export function hasEmptyLineBefore(decl) { 2 | if (/\r?\n\s*\r?\n/.test(decl.raw('before'))) { 3 | return true; 4 | } 5 | 6 | const prevNode = decl.prev(); 7 | 8 | if (!prevNode) { 9 | return false; 10 | } 11 | 12 | if (prevNode.type !== 'comment') { 13 | return false; 14 | } 15 | 16 | if (/\r?\n\s*\r?\n/.test(prevNode.raw('before'))) { 17 | return true; 18 | } 19 | 20 | return false; 21 | } 22 | -------------------------------------------------------------------------------- /rules/properties-order/index.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import { getContainingNode } from '../../utils/getContainingNode.js'; 3 | import { isRuleWithNodes } from '../../utils/isRuleWithNodes.js'; 4 | import { isNumber } from '../../utils/validateType.js'; 5 | import { checkNodeForOrder } from './checkNodeForOrder.js'; 6 | import { checkNodeForEmptyLines } from './checkNodeForEmptyLines.js'; 7 | import { createOrderInfo } from './createOrderInfo.js'; 8 | import { validatePrimaryOption } from './validatePrimaryOption.js'; 9 | 10 | import { ruleName } from './ruleName.js'; 11 | import { messages } from './messages.js'; 12 | 13 | export function rule(primaryOption, options = {}, context = {}) { 14 | return function ruleBody(root, result) { 15 | let validOptions = stylelint.utils.validateOptions( 16 | result, 17 | ruleName, 18 | { 19 | actual: primaryOption, 20 | possible: validatePrimaryOption, 21 | }, 22 | { 23 | actual: options, 24 | possible: { 25 | unspecified: ['top', 'bottom', 'ignore', 'bottomAlphabetical'], 26 | emptyLineBeforeUnspecified: ['always', 'never', 'threshold'], 27 | emptyLineMinimumPropertyThreshold: isNumber, 28 | }, 29 | optional: true, 30 | }, 31 | ); 32 | 33 | if (!validOptions) { 34 | return; 35 | } 36 | 37 | let expectedOrder = createOrderInfo(primaryOption); 38 | 39 | let processedParents = []; 40 | 41 | // Check all rules and at-rules recursively 42 | root.walk(function processRulesAndAtrules(input) { 43 | let node = getContainingNode(input); 44 | 45 | // Avoid warnings duplication, caused by interfering in `root.walk()` algorigthm with `getContainingNode()` 46 | if (processedParents.includes(node)) { 47 | return; 48 | } 49 | 50 | processedParents.push(node); 51 | 52 | if (isRuleWithNodes(node)) { 53 | checkNodeForOrder({ 54 | node, 55 | primaryOption, 56 | unspecified: options.unspecified || 'ignore', 57 | result, 58 | expectedOrder, 59 | }); 60 | 61 | checkNodeForEmptyLines({ 62 | node, 63 | context, 64 | emptyLineBeforeUnspecified: options.emptyLineBeforeUnspecified, 65 | emptyLineMinimumPropertyThreshold: 66 | options.emptyLineMinimumPropertyThreshold || 0, 67 | expectedOrder, 68 | primaryOption, 69 | result, 70 | }); 71 | } 72 | }); 73 | }; 74 | } 75 | 76 | rule.primaryOptionArray = true; 77 | rule.ruleName = ruleName; 78 | rule.messages = messages; 79 | rule.meta = { 80 | fixable: true, 81 | url: 'https://github.com/hudochenkov/stylelint-order/blob/master/rules/properties-order/README.md', 82 | }; 83 | -------------------------------------------------------------------------------- /rules/properties-order/messages.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import { ruleName } from './ruleName.js'; 3 | 4 | export const messages = stylelint.utils.ruleMessages(ruleName, { 5 | expected: (first, second, groupName) => 6 | `Expected "${first}" to come before "${second}"${ 7 | groupName ? ` in group "${groupName}"` : '' 8 | }`, 9 | expectedEmptyLineBefore: (property) => `Expected an empty line before property "${property}"`, 10 | rejectedEmptyLineBefore: (property) => `Unexpected empty line before property "${property}"`, 11 | }); 12 | -------------------------------------------------------------------------------- /rules/properties-order/removeEmptyLinesBefore.js: -------------------------------------------------------------------------------- 1 | // Remove empty lines before a node. Mutates the node. 2 | export function removeEmptyLinesBefore(node, newline) { 3 | node.raws.before = node.raws.before.replace(/(\r?\n\s*\r?\n)+/g, newline); 4 | 5 | return node; 6 | } 7 | -------------------------------------------------------------------------------- /rules/properties-order/ruleName.js: -------------------------------------------------------------------------------- 1 | import { namespace } from '../../utils/namespace.js'; 2 | 3 | export const ruleName = namespace('properties-order'); 4 | -------------------------------------------------------------------------------- /rules/properties-order/tests/addEmptyLineBefore.test.js: -------------------------------------------------------------------------------- 1 | import { addEmptyLineBefore } from '../addEmptyLineBefore.js'; 2 | import postcss from 'postcss'; 3 | 4 | function addEmptyLine(css, lineEnding) { 5 | const root = postcss.parse(css); 6 | 7 | addEmptyLineBefore(root.nodes[1], lineEnding); 8 | 9 | return root.toString(); 10 | } 11 | 12 | describe('addEmptyLineBefore', () => { 13 | it('adds single newline to the newline at the beginning', () => { 14 | expect(addEmptyLine('a {}\n b{}', '\n')).toBe('a {}\n\n b{}'); 15 | }); 16 | 17 | it('adds single newline to newline at the beginning with CRLF', () => { 18 | expect(addEmptyLine('a {}\r\n b{}', '\r\n')).toBe('a {}\r\n\r\n b{}'); 19 | }); 20 | 21 | it('adds single newline to newline at the end', () => { 22 | expect(addEmptyLine('a {}\t\nb{}', '\n')).toBe('a {}\t\n\nb{}'); 23 | }); 24 | 25 | it('adds single newline to newline at the end with CRLF', () => { 26 | expect(addEmptyLine('a {}\t\r\nb{}', '\r\n')).toBe('a {}\t\r\n\r\nb{}'); 27 | }); 28 | 29 | it('adds single newline to newline in the middle', () => { 30 | expect(addEmptyLine('a {} \n\tb{}', '\n')).toBe('a {} \n\n\tb{}'); 31 | }); 32 | 33 | it('adds single newline to newline in the middle with CRLF', () => { 34 | expect(addEmptyLine('a {} \r\n\tb{}', '\r\n')).toBe('a {} \r\n\r\n\tb{}'); 35 | }); 36 | 37 | it("adds two newlines if there aren't any existing newlines", () => { 38 | expect(addEmptyLine('a {} b{}', '\n')).toBe('a {}\n\n b{}'); 39 | }); 40 | 41 | it("adds two newlines if there aren't any existing newlines with CRLF", () => { 42 | expect(addEmptyLine('a {} b{}', '\r\n')).toBe('a {}\r\n\r\n b{}'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /rules/properties-order/tests/createFlatOrder.test.js: -------------------------------------------------------------------------------- 1 | import { createFlatOrder } from '../createFlatOrder.js'; 2 | 3 | describe('createFlatOrder', () => { 4 | it('valid group and declaration', () => { 5 | const config = [ 6 | 'height', 7 | 'width', 8 | { 9 | emptyLineBefore: 'always', 10 | order: 'strict', 11 | properties: ['display'], 12 | }, 13 | ]; 14 | 15 | const expected = ['height', 'width', 'display']; 16 | 17 | expect(createFlatOrder(config)).toEqual(expected); 18 | }); 19 | 20 | it('valid groups with emptyLineBefore', () => { 21 | const config = [ 22 | { 23 | emptyLineBefore: 'always', 24 | order: 'flexible', 25 | properties: ['border-bottom', 'font-style'], 26 | }, 27 | { 28 | emptyLineBefore: 'never', 29 | order: 'strict', 30 | properties: ['position'], 31 | }, 32 | { 33 | emptyLineBefore: 'always', 34 | order: 'strict', 35 | properties: ['display'], 36 | }, 37 | ]; 38 | 39 | const expected = ['border-bottom', 'font-style', 'position', 'display']; 40 | 41 | expect(createFlatOrder(config)).toEqual(expected); 42 | }); 43 | 44 | it('valid groups (one without emptyLineBefore)', () => { 45 | const config = [ 46 | { 47 | properties: ['display'], 48 | }, 49 | { 50 | emptyLineBefore: 'always', 51 | order: 'strict', 52 | properties: ['border'], 53 | }, 54 | ]; 55 | 56 | const expected = ['display', 'border']; 57 | 58 | expect(createFlatOrder(config)).toEqual(expected); 59 | }); 60 | 61 | it('empty properties', () => { 62 | const config = [ 63 | { 64 | emptyLineBefore: 'always', 65 | properties: [], 66 | }, 67 | ]; 68 | 69 | const expected = []; 70 | 71 | expect(createFlatOrder(config)).toEqual(expected); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /rules/properties-order/tests/empty-line-before-unspecified.js: -------------------------------------------------------------------------------- 1 | import { rule } from '../index.js'; 2 | 3 | const { ruleName, messages } = rule; 4 | 5 | testRule({ 6 | ruleName, 7 | config: [ 8 | ['height', 'width'], 9 | { 10 | unspecified: 'bottom', 11 | emptyLineBeforeUnspecified: 'always', 12 | }, 13 | ], 14 | fix: true, 15 | 16 | accept: [ 17 | { 18 | description: '1', 19 | code: ` 20 | a { 21 | height: 20px; 22 | 23 | width: 20px; 24 | 25 | font-style: italic; 26 | } 27 | `, 28 | }, 29 | { 30 | description: '2', 31 | code: ` 32 | a { 33 | height: 20px; 34 | width: 20px; 35 | 36 | font-style: italic; 37 | } 38 | `, 39 | }, 40 | { 41 | description: '3', 42 | code: ` 43 | a { 44 | height: 20px; 45 | 46 | font-style: italic; 47 | } 48 | `, 49 | }, 50 | { 51 | description: '4', 52 | code: ` 53 | a { 54 | font-style: italic; 55 | } 56 | `, 57 | }, 58 | { 59 | description: '5', 60 | code: ` 61 | a { 62 | } 63 | `, 64 | }, 65 | { 66 | description: '6', 67 | code: ` 68 | a { 69 | height: 20px; 70 | /* other props */ 71 | font-style: italic; 72 | } 73 | `, 74 | }, 75 | { 76 | description: '7', 77 | code: ` 78 | a { 79 | height: 20px; 80 | 81 | /* other props */ 82 | font-style: italic; 83 | } 84 | `, 85 | }, 86 | { 87 | description: '8', 88 | code: ` 89 | a { 90 | height: 20px; 91 | width: 20px; 92 | 93 | b { 94 | font-style: italic; 95 | } 96 | } 97 | `, 98 | }, 99 | { 100 | description: '9', 101 | code: ` 102 | a { 103 | height: 20px; 104 | width: 20px; 105 | b { 106 | font-style: italic; 107 | } 108 | } 109 | `, 110 | }, 111 | ], 112 | 113 | reject: [ 114 | { 115 | description: '10', 116 | code: ` 117 | a { 118 | height: 20px; 119 | 120 | width: 20px; 121 | font-style: italic; 122 | } 123 | `, 124 | fixed: ` 125 | a { 126 | height: 20px; 127 | 128 | width: 20px; 129 | 130 | font-style: italic; 131 | } 132 | `, 133 | message: messages.expectedEmptyLineBefore('font-style'), 134 | }, 135 | { 136 | description: '11', 137 | code: ` 138 | a { 139 | height: 20px; 140 | width: 20px; 141 | font-style: italic; 142 | } 143 | `, 144 | fixed: ` 145 | a { 146 | height: 20px; 147 | width: 20px; 148 | 149 | font-style: italic; 150 | } 151 | `, 152 | message: messages.expectedEmptyLineBefore('font-style'), 153 | }, 154 | { 155 | description: '12', 156 | code: ` 157 | a { 158 | height: 20px; 159 | font-style: italic; 160 | } 161 | `, 162 | fixed: ` 163 | a { 164 | height: 20px; 165 | 166 | font-style: italic; 167 | } 168 | `, 169 | message: messages.expectedEmptyLineBefore('font-style'), 170 | }, 171 | { 172 | description: '12', 173 | code: ` 174 | a { 175 | 176 | font-style: italic; 177 | } 178 | `, 179 | fixed: ` 180 | a { 181 | font-style: italic; 182 | } 183 | `, 184 | message: messages.rejectedEmptyLineBefore('font-style'), 185 | }, 186 | ], 187 | }); 188 | 189 | testRule({ 190 | ruleName, 191 | config: [ 192 | ['height', 'width'], 193 | { 194 | unspecified: 'bottom', 195 | emptyLineBeforeUnspecified: 'never', 196 | }, 197 | ], 198 | fix: true, 199 | 200 | accept: [ 201 | { 202 | description: '13', 203 | code: ` 204 | a { 205 | height: 20px; 206 | 207 | width: 20px; 208 | font-style: italic; 209 | } 210 | `, 211 | }, 212 | { 213 | description: '14', 214 | code: ` 215 | a { 216 | height: 20px; 217 | width: 20px; 218 | font-style: italic; 219 | } 220 | `, 221 | }, 222 | { 223 | description: '15', 224 | code: ` 225 | a { 226 | height: 20px; 227 | font-style: italic; 228 | } 229 | `, 230 | }, 231 | { 232 | description: '16', 233 | code: ` 234 | a { 235 | font-style: italic; 236 | } 237 | `, 238 | }, 239 | { 240 | description: '17', 241 | code: ` 242 | a { 243 | } 244 | `, 245 | }, 246 | { 247 | description: '18', 248 | code: ` 249 | a { 250 | height: 20px; 251 | /* other props */ 252 | font-style: italic; 253 | } 254 | `, 255 | }, 256 | { 257 | description: '19', 258 | code: ` 259 | a { 260 | height: 20px; 261 | 262 | /* other props */ 263 | font-style: italic; 264 | } 265 | `, 266 | }, 267 | { 268 | description: '20', 269 | code: ` 270 | a { 271 | height: 20px; 272 | width: 20px; 273 | 274 | b { 275 | font-style: italic; 276 | } 277 | } 278 | `, 279 | }, 280 | { 281 | description: '21', 282 | code: ` 283 | a { 284 | height: 20px; 285 | width: 20px; 286 | b { 287 | font-style: italic; 288 | } 289 | } 290 | `, 291 | }, 292 | ], 293 | 294 | reject: [ 295 | { 296 | description: '22', 297 | code: ` 298 | a { 299 | height: 20px; 300 | 301 | width: 20px; 302 | 303 | font-style: italic; 304 | } 305 | `, 306 | fixed: ` 307 | a { 308 | height: 20px; 309 | 310 | width: 20px; 311 | font-style: italic; 312 | } 313 | `, 314 | message: messages.rejectedEmptyLineBefore('font-style'), 315 | }, 316 | { 317 | description: '23', 318 | code: ` 319 | a { 320 | height: 20px; 321 | width: 20px; 322 | 323 | font-style: italic; 324 | } 325 | `, 326 | fixed: ` 327 | a { 328 | height: 20px; 329 | width: 20px; 330 | font-style: italic; 331 | } 332 | `, 333 | message: messages.rejectedEmptyLineBefore('font-style'), 334 | }, 335 | { 336 | description: '24', 337 | code: ` 338 | a { 339 | height: 20px; 340 | 341 | font-style: italic; 342 | } 343 | `, 344 | fixed: ` 345 | a { 346 | height: 20px; 347 | font-style: italic; 348 | } 349 | `, 350 | message: messages.rejectedEmptyLineBefore('font-style'), 351 | }, 352 | ], 353 | }); 354 | -------------------------------------------------------------------------------- /rules/properties-order/tests/empty-line-before.js: -------------------------------------------------------------------------------- 1 | import { rule } from '../index.js'; 2 | 3 | const { ruleName, messages } = rule; 4 | 5 | testRule({ 6 | ruleName, 7 | config: [ 8 | [ 9 | { 10 | emptyLineBefore: 'always', 11 | properties: ['display'], 12 | }, 13 | { 14 | emptyLineBefore: 'always', 15 | properties: ['position'], 16 | }, 17 | { 18 | emptyLineBefore: 'always', 19 | properties: ['border-bottom', 'font-style'], 20 | }, 21 | ], 22 | ], 23 | fix: true, 24 | 25 | accept: [ 26 | { 27 | description: '1', 28 | code: ` 29 | a { 30 | display: none; 31 | 32 | position: absolute; 33 | 34 | border-bottom: 1px solid red; 35 | font-style: italic; 36 | } 37 | `, 38 | }, 39 | { 40 | description: '2', 41 | code: ` 42 | a { 43 | display: none; 44 | 45 | position: absolute; 46 | 47 | font-style: italic; 48 | } 49 | `, 50 | }, 51 | { 52 | description: '3', 53 | code: ` 54 | a { 55 | display: none; 56 | 57 | font-style: italic; 58 | } 59 | `, 60 | }, 61 | { 62 | description: '4', 63 | code: ` 64 | a { 65 | position: absolute; 66 | 67 | border-bottom: 1px solid red; 68 | } 69 | `, 70 | }, 71 | { 72 | description: '5', 73 | code: ` 74 | a { 75 | display: none; 76 | 77 | border-bottom: 1px solid red; 78 | } 79 | `, 80 | }, 81 | { 82 | description: '6', 83 | code: ` 84 | a { 85 | display: none; /* comment */ 86 | 87 | position: absolute; 88 | } 89 | `, 90 | }, 91 | { 92 | description: '7', 93 | code: ` 94 | a { 95 | display: none; 96 | /* comment */ 97 | position: absolute; 98 | } 99 | `, 100 | }, 101 | { 102 | description: '8', 103 | code: ` 104 | a { 105 | /* comment */ 106 | display: none; 107 | 108 | position: absolute; 109 | } 110 | `, 111 | }, 112 | { 113 | description: '9', 114 | code: ` 115 | a { 116 | /* comment */ 117 | display: none; 118 | 119 | /* comment */ 120 | position: absolute; 121 | } 122 | `, 123 | }, 124 | { 125 | description: '12', 126 | code: ` 127 | a { 128 | --display: none; 129 | 130 | position: absolute; 131 | } 132 | `, 133 | }, 134 | { 135 | description: '13', 136 | code: ` 137 | a { 138 | --display: none; 139 | position: absolute; 140 | } 141 | `, 142 | }, 143 | { 144 | description: '13.1', 145 | code: ` 146 | a { 147 | $display: none; 148 | position: absolute; 149 | } 150 | `, 151 | }, 152 | { 153 | description: '13.2', 154 | code: ` 155 | a { 156 | $display: none; 157 | 158 | position: absolute; 159 | } 160 | `, 161 | }, 162 | { 163 | description: '13.3', 164 | code: ` 165 | a { 166 | position: absolute; 167 | $display: none; 168 | } 169 | `, 170 | }, 171 | { 172 | description: '13.4', 173 | code: ` 174 | a { 175 | position: absolute; 176 | 177 | $display: none; 178 | } 179 | `, 180 | }, 181 | ], 182 | 183 | reject: [ 184 | { 185 | description: '14', 186 | code: ` 187 | a { 188 | display: none; 189 | position: absolute; 190 | 191 | border-bottom: 1px solid red; 192 | font-style: italic; 193 | } 194 | `, 195 | fixed: ` 196 | a { 197 | display: none; 198 | 199 | position: absolute; 200 | 201 | border-bottom: 1px solid red; 202 | font-style: italic; 203 | } 204 | `, 205 | message: messages.expectedEmptyLineBefore('position'), 206 | }, 207 | { 208 | description: '15', 209 | code: ` 210 | a { 211 | display: none; 212 | 213 | position: absolute; 214 | border-bottom: 1px solid red; 215 | font-style: italic; 216 | } 217 | `, 218 | fixed: ` 219 | a { 220 | display: none; 221 | 222 | position: absolute; 223 | 224 | border-bottom: 1px solid red; 225 | font-style: italic; 226 | } 227 | `, 228 | message: messages.expectedEmptyLineBefore('border-bottom'), 229 | }, 230 | { 231 | description: '16', 232 | code: ` 233 | a { 234 | display: none; 235 | position: absolute; 236 | 237 | font-style: italic; 238 | } 239 | `, 240 | fixed: ` 241 | a { 242 | display: none; 243 | 244 | position: absolute; 245 | 246 | font-style: italic; 247 | } 248 | `, 249 | message: messages.expectedEmptyLineBefore('position'), 250 | }, 251 | { 252 | description: '17', 253 | code: ` 254 | a { 255 | display: none; 256 | font-style: italic; 257 | } 258 | `, 259 | fixed: ` 260 | a { 261 | display: none; 262 | 263 | font-style: italic; 264 | } 265 | `, 266 | message: messages.expectedEmptyLineBefore('font-style'), 267 | }, 268 | { 269 | description: '18', 270 | code: ` 271 | a { 272 | position: absolute; 273 | border-bottom: 1px solid red; 274 | } 275 | `, 276 | fixed: ` 277 | a { 278 | position: absolute; 279 | 280 | border-bottom: 1px solid red; 281 | } 282 | `, 283 | message: messages.expectedEmptyLineBefore('border-bottom'), 284 | }, 285 | { 286 | description: '19', 287 | code: ` 288 | a { 289 | display: none; 290 | border-bottom: 1px solid red; 291 | } 292 | `, 293 | fixed: ` 294 | a { 295 | display: none; 296 | 297 | border-bottom: 1px solid red; 298 | } 299 | `, 300 | message: messages.expectedEmptyLineBefore('border-bottom'), 301 | }, 302 | { 303 | description: '20', 304 | code: ` 305 | a { 306 | display: none; /* comment */ 307 | position: absolute; 308 | } 309 | `, 310 | fixed: ` 311 | a { 312 | display: none; /* comment */ 313 | 314 | position: absolute; 315 | } 316 | `, 317 | message: messages.expectedEmptyLineBefore('position'), 318 | }, 319 | { 320 | description: '21', 321 | code: ` 322 | a { 323 | /* comment */ 324 | display: none; 325 | position: absolute; 326 | } 327 | `, 328 | fixed: ` 329 | a { 330 | /* comment */ 331 | display: none; 332 | 333 | position: absolute; 334 | } 335 | `, 336 | message: messages.expectedEmptyLineBefore('position'), 337 | }, 338 | { 339 | description: '22', 340 | code: ` 341 | a { 342 | 343 | display: none; 344 | } 345 | `, 346 | fixed: ` 347 | a { 348 | display: none; 349 | } 350 | `, 351 | message: messages.rejectedEmptyLineBefore('display'), 352 | }, 353 | { 354 | description: '23', 355 | code: ` 356 | a { 357 | 358 | position: absolute; 359 | } 360 | `, 361 | fixed: ` 362 | a { 363 | position: absolute; 364 | } 365 | `, 366 | message: messages.rejectedEmptyLineBefore('position'), 367 | }, 368 | { 369 | description: '24', 370 | code: ` 371 | a { 372 | 373 | font-style: italic; 374 | } 375 | `, 376 | fixed: ` 377 | a { 378 | font-style: italic; 379 | } 380 | `, 381 | message: messages.rejectedEmptyLineBefore('font-style'), 382 | }, 383 | ], 384 | }); 385 | 386 | testRule({ 387 | ruleName, 388 | config: [ 389 | [ 390 | { 391 | emptyLineBefore: 'always', 392 | properties: ['display'], 393 | }, 394 | { 395 | emptyLineBefore: 'always', 396 | properties: ['position'], 397 | }, 398 | { 399 | emptyLineBefore: 'always', 400 | properties: ['border-bottom', 'font-style'], 401 | }, 402 | ], 403 | ], 404 | 405 | accept: [ 406 | { 407 | description: '10', 408 | code: ` 409 | a { 410 | display: none; 411 | 412 | @media (min-width: 100px) {} 413 | 414 | position: absolute; 415 | } 416 | `, 417 | }, 418 | { 419 | description: '11', 420 | code: ` 421 | a { 422 | display: none; 423 | @media (min-width: 100px) {} 424 | position: absolute; 425 | } 426 | `, 427 | }, 428 | ], 429 | }); 430 | 431 | testRule({ 432 | ruleName, 433 | config: [ 434 | [ 435 | { 436 | emptyLineBefore: 'never', 437 | properties: ['display'], 438 | }, 439 | { 440 | emptyLineBefore: 'never', 441 | properties: ['position'], 442 | }, 443 | { 444 | emptyLineBefore: 'never', 445 | properties: ['border-bottom', 'font-style'], 446 | }, 447 | ], 448 | ], 449 | fix: true, 450 | 451 | accept: [ 452 | { 453 | description: '22', 454 | code: ` 455 | a { 456 | display: none; 457 | position: absolute; 458 | border-bottom: 1px solid red; 459 | font-style: italic; 460 | } 461 | `, 462 | }, 463 | { 464 | description: '23', 465 | code: ` 466 | a { 467 | display: none; 468 | position: absolute; 469 | font-style: italic; 470 | } 471 | `, 472 | }, 473 | { 474 | description: '24', 475 | code: ` 476 | a { 477 | display: none; 478 | font-style: italic; 479 | } 480 | `, 481 | }, 482 | { 483 | description: '25', 484 | code: ` 485 | a { 486 | position: absolute; 487 | border-bottom: 1px solid red; 488 | } 489 | `, 490 | }, 491 | { 492 | description: '26', 493 | code: ` 494 | a { 495 | display: none; 496 | border-bottom: 1px solid red; 497 | } 498 | `, 499 | }, 500 | { 501 | description: '27', 502 | code: ` 503 | a { 504 | display: none; /* comment */ 505 | position: absolute; 506 | } 507 | `, 508 | }, 509 | { 510 | description: '28', 511 | code: ` 512 | a { 513 | display: none; 514 | 515 | /* comment */ 516 | position: absolute; 517 | } 518 | `, 519 | }, 520 | { 521 | description: '29', 522 | code: ` 523 | a { 524 | /* comment */ 525 | display: none; 526 | position: absolute; 527 | } 528 | `, 529 | }, 530 | { 531 | description: '30', 532 | code: ` 533 | a { 534 | /* comment */ 535 | display: none; 536 | 537 | /* comment */ 538 | position: absolute; 539 | } 540 | `, 541 | }, 542 | { 543 | description: '33', 544 | code: ` 545 | a { 546 | --display: none; 547 | 548 | position: absolute; 549 | } 550 | `, 551 | }, 552 | { 553 | description: '34', 554 | code: ` 555 | a { 556 | --display: none; 557 | position: absolute; 558 | } 559 | `, 560 | }, 561 | ], 562 | 563 | reject: [ 564 | { 565 | description: '35', 566 | code: ` 567 | a { 568 | display: none; 569 | position: absolute; 570 | 571 | border-bottom: 1px solid red; 572 | font-style: italic; 573 | } 574 | `, 575 | fixed: ` 576 | a { 577 | display: none; 578 | position: absolute; 579 | border-bottom: 1px solid red; 580 | font-style: italic; 581 | } 582 | `, 583 | message: messages.rejectedEmptyLineBefore('border-bottom'), 584 | }, 585 | { 586 | description: '36', 587 | code: ` 588 | a { 589 | display: none; 590 | 591 | position: absolute; 592 | border-bottom: 1px solid red; 593 | font-style: italic; 594 | } 595 | `, 596 | fixed: ` 597 | a { 598 | display: none; 599 | position: absolute; 600 | border-bottom: 1px solid red; 601 | font-style: italic; 602 | } 603 | `, 604 | message: messages.rejectedEmptyLineBefore('position'), 605 | }, 606 | { 607 | description: '37', 608 | code: ` 609 | a { 610 | display: none; 611 | position: absolute; 612 | 613 | font-style: italic; 614 | } 615 | `, 616 | fixed: ` 617 | a { 618 | display: none; 619 | position: absolute; 620 | font-style: italic; 621 | } 622 | `, 623 | message: messages.rejectedEmptyLineBefore('font-style'), 624 | }, 625 | { 626 | description: '38', 627 | code: ` 628 | a { 629 | display: none; 630 | 631 | font-style: italic; 632 | } 633 | `, 634 | fixed: ` 635 | a { 636 | display: none; 637 | font-style: italic; 638 | } 639 | `, 640 | message: messages.rejectedEmptyLineBefore('font-style'), 641 | }, 642 | { 643 | description: '39', 644 | code: ` 645 | a { 646 | position: absolute; 647 | 648 | border-bottom: 1px solid red; 649 | } 650 | `, 651 | fixed: ` 652 | a { 653 | position: absolute; 654 | border-bottom: 1px solid red; 655 | } 656 | `, 657 | message: messages.rejectedEmptyLineBefore('border-bottom'), 658 | }, 659 | { 660 | description: '40', 661 | code: ` 662 | a { 663 | display: none; 664 | 665 | border-bottom: 1px solid red; 666 | } 667 | `, 668 | fixed: ` 669 | a { 670 | display: none; 671 | border-bottom: 1px solid red; 672 | } 673 | `, 674 | message: messages.rejectedEmptyLineBefore('border-bottom'), 675 | }, 676 | { 677 | description: '41', 678 | code: ` 679 | a { 680 | display: none; /* comment */ 681 | 682 | position: absolute; 683 | } 684 | `, 685 | fixed: ` 686 | a { 687 | display: none; /* comment */ 688 | position: absolute; 689 | } 690 | `, 691 | message: messages.rejectedEmptyLineBefore('position'), 692 | }, 693 | { 694 | description: '42', 695 | code: ` 696 | a { 697 | /* comment */ 698 | display: none; 699 | 700 | position: absolute; 701 | } 702 | `, 703 | fixed: ` 704 | a { 705 | /* comment */ 706 | display: none; 707 | position: absolute; 708 | } 709 | `, 710 | message: messages.rejectedEmptyLineBefore('position'), 711 | }, 712 | ], 713 | }); 714 | 715 | testRule({ 716 | ruleName, 717 | config: [ 718 | [ 719 | { 720 | emptyLineBefore: 'never', 721 | properties: ['display'], 722 | }, 723 | { 724 | emptyLineBefore: 'never', 725 | properties: ['position'], 726 | }, 727 | { 728 | emptyLineBefore: 'never', 729 | properties: ['border-bottom', 'font-style'], 730 | }, 731 | ], 732 | ], 733 | 734 | accept: [ 735 | { 736 | description: '31', 737 | code: ` 738 | a { 739 | display: none; 740 | 741 | @media (min-width: 100px) {} 742 | 743 | position: absolute; 744 | } 745 | `, 746 | }, 747 | { 748 | description: '32', 749 | code: ` 750 | a { 751 | display: none; 752 | @media (min-width: 100px) {} 753 | position: absolute; 754 | } 755 | `, 756 | }, 757 | ], 758 | }); 759 | 760 | testRule({ 761 | ruleName, 762 | config: [ 763 | [ 764 | { 765 | emptyLineBefore: 'always', 766 | properties: ['border-bottom', 'font-style'], 767 | }, 768 | { 769 | emptyLineBefore: 'never', 770 | properties: ['position'], 771 | }, 772 | { 773 | emptyLineBefore: 'always', 774 | properties: ['display'], 775 | }, 776 | ], 777 | ], 778 | fix: true, 779 | 780 | accept: [ 781 | { 782 | description: '43', 783 | code: ` 784 | a { 785 | border-bottom: 1px solid red; 786 | font-style: italic; 787 | position: absolute; 788 | 789 | display: none; 790 | } 791 | `, 792 | }, 793 | { 794 | description: '44', 795 | code: ` 796 | a { 797 | font-style: italic; 798 | position: absolute; 799 | 800 | display: none; 801 | } 802 | `, 803 | }, 804 | { 805 | description: '45', 806 | code: ` 807 | a { 808 | font-style: italic; 809 | 810 | display: none; 811 | } 812 | `, 813 | }, 814 | { 815 | description: '46', 816 | code: ` 817 | a { 818 | border-bottom: 1px solid red; 819 | position: absolute; 820 | } 821 | `, 822 | }, 823 | { 824 | description: '47', 825 | code: ` 826 | a { 827 | border-bottom: 1px solid red; 828 | 829 | display: none; 830 | } 831 | `, 832 | }, 833 | { 834 | description: '48', 835 | code: ` 836 | a { 837 | position: absolute; /* comment */ 838 | 839 | display: none; 840 | } 841 | `, 842 | }, 843 | { 844 | description: '49', 845 | code: ` 846 | a { 847 | position: absolute; 848 | 849 | /* comment */ 850 | display: none; 851 | } 852 | `, 853 | }, 854 | { 855 | description: '50', 856 | code: ` 857 | a { 858 | position: absolute; 859 | /* comment */ 860 | display: none; 861 | } 862 | `, 863 | }, 864 | { 865 | description: '51', 866 | code: ` 867 | a { 868 | /* comment */ 869 | position: absolute; 870 | 871 | /* comment */ 872 | display: none; 873 | } 874 | `, 875 | }, 876 | { 877 | description: '54', 878 | code: ` 879 | a { 880 | --display: none; 881 | position: absolute; 882 | } 883 | `, 884 | }, 885 | { 886 | description: '55', 887 | code: ` 888 | a { 889 | --display: none; 890 | 891 | position: absolute; 892 | } 893 | `, 894 | }, 895 | ], 896 | 897 | reject: [ 898 | { 899 | description: '56', 900 | code: ` 901 | a { 902 | border-bottom: 1px solid red; 903 | font-style: italic; 904 | position: absolute; 905 | display: none; 906 | } 907 | `, 908 | fixed: ` 909 | a { 910 | border-bottom: 1px solid red; 911 | font-style: italic; 912 | position: absolute; 913 | 914 | display: none; 915 | } 916 | `, 917 | message: messages.expectedEmptyLineBefore('display'), 918 | }, 919 | { 920 | description: '57', 921 | code: ` 922 | a { 923 | border-bottom: 1px solid red; 924 | font-style: italic; 925 | 926 | position: absolute; 927 | 928 | display: none; 929 | } 930 | `, 931 | fixed: ` 932 | a { 933 | border-bottom: 1px solid red; 934 | font-style: italic; 935 | position: absolute; 936 | 937 | display: none; 938 | } 939 | `, 940 | message: messages.rejectedEmptyLineBefore('position'), 941 | }, 942 | { 943 | description: '58', 944 | code: ` 945 | a { 946 | font-style: italic; 947 | position: absolute; 948 | display: none; 949 | } 950 | `, 951 | fixed: ` 952 | a { 953 | font-style: italic; 954 | position: absolute; 955 | 956 | display: none; 957 | } 958 | `, 959 | message: messages.expectedEmptyLineBefore('display'), 960 | }, 961 | { 962 | description: '59', 963 | code: ` 964 | a { 965 | font-style: italic; 966 | display: none; 967 | } 968 | `, 969 | fixed: ` 970 | a { 971 | font-style: italic; 972 | 973 | display: none; 974 | } 975 | `, 976 | message: messages.expectedEmptyLineBefore('display'), 977 | }, 978 | { 979 | description: '60', 980 | code: ` 981 | a { 982 | border-bottom: 1px solid red; 983 | 984 | position: absolute; 985 | } 986 | `, 987 | fixed: ` 988 | a { 989 | border-bottom: 1px solid red; 990 | position: absolute; 991 | } 992 | `, 993 | message: messages.rejectedEmptyLineBefore('position'), 994 | }, 995 | { 996 | description: '61', 997 | code: ` 998 | a { 999 | border-bottom: 1px solid red; 1000 | display: none; 1001 | } 1002 | `, 1003 | fixed: ` 1004 | a { 1005 | border-bottom: 1px solid red; 1006 | 1007 | display: none; 1008 | } 1009 | `, 1010 | message: messages.expectedEmptyLineBefore('display'), 1011 | }, 1012 | { 1013 | description: '62', 1014 | code: ` 1015 | a { 1016 | position: absolute; /* comment */ 1017 | display: none; 1018 | } 1019 | `, 1020 | fixed: ` 1021 | a { 1022 | position: absolute; /* comment */ 1023 | 1024 | display: none; 1025 | } 1026 | `, 1027 | message: messages.expectedEmptyLineBefore('display'), 1028 | }, 1029 | ], 1030 | }); 1031 | 1032 | testRule({ 1033 | ruleName, 1034 | config: [ 1035 | [ 1036 | { 1037 | emptyLineBefore: 'always', 1038 | properties: ['border-bottom', 'font-style'], 1039 | }, 1040 | { 1041 | emptyLineBefore: 'never', 1042 | properties: ['position'], 1043 | }, 1044 | { 1045 | emptyLineBefore: 'always', 1046 | properties: ['display'], 1047 | }, 1048 | ], 1049 | ], 1050 | 1051 | accept: [ 1052 | { 1053 | description: '52', 1054 | code: ` 1055 | a { 1056 | position: absolute; 1057 | 1058 | @media (min-width: 100px) {} 1059 | 1060 | display: none; 1061 | } 1062 | `, 1063 | }, 1064 | { 1065 | description: '53', 1066 | code: ` 1067 | a { 1068 | position: absolute; 1069 | @media (min-width: 100px) {} 1070 | display: none; 1071 | } 1072 | `, 1073 | }, 1074 | ], 1075 | }); 1076 | 1077 | testRule({ 1078 | ruleName, 1079 | config: [ 1080 | [ 1081 | 'height', 1082 | 'width', 1083 | { 1084 | emptyLineBefore: 'always', 1085 | properties: ['display'], 1086 | }, 1087 | ], 1088 | ], 1089 | fix: true, 1090 | 1091 | accept: [ 1092 | { 1093 | description: '63', 1094 | code: ` 1095 | a { 1096 | height: 10px; 1097 | width: 10px; 1098 | 1099 | display: none; 1100 | } 1101 | `, 1102 | }, 1103 | { 1104 | description: '64', 1105 | code: ` 1106 | a { 1107 | height: 10px; 1108 | 1109 | display: none; 1110 | } 1111 | `, 1112 | }, 1113 | { 1114 | description: '65', 1115 | code: ` 1116 | a { 1117 | display: none; 1118 | } 1119 | `, 1120 | }, 1121 | { 1122 | description: '67', 1123 | code: ` 1124 | a { 1125 | height: 10px; 1126 | } 1127 | `, 1128 | }, 1129 | { 1130 | description: '68', 1131 | code: ` 1132 | a { 1133 | height: 10px; 1134 | width: 10px; /* comment */ 1135 | 1136 | display: none; 1137 | } 1138 | `, 1139 | }, 1140 | { 1141 | description: '69', 1142 | code: ` 1143 | a { 1144 | height: 10px; 1145 | width: 10px; 1146 | /* comment */ 1147 | display: none; 1148 | } 1149 | `, 1150 | }, 1151 | { 1152 | description: '70', 1153 | code: ` 1154 | a { 1155 | height: 10px; 1156 | width: 10px; 1157 | 1158 | /* comment */ 1159 | display: none; 1160 | } 1161 | `, 1162 | }, 1163 | ], 1164 | 1165 | reject: [ 1166 | { 1167 | description: '71', 1168 | code: ` 1169 | a { 1170 | height: 10px; 1171 | width: 10px; 1172 | display: none; 1173 | } 1174 | `, 1175 | fixed: ` 1176 | a { 1177 | height: 10px; 1178 | width: 10px; 1179 | 1180 | display: none; 1181 | } 1182 | `, 1183 | message: messages.expectedEmptyLineBefore('display'), 1184 | }, 1185 | { 1186 | description: '72', 1187 | code: ` 1188 | a { 1189 | height: 10px; 1190 | display: none; 1191 | } 1192 | `, 1193 | fixed: ` 1194 | a { 1195 | height: 10px; 1196 | 1197 | display: none; 1198 | } 1199 | `, 1200 | message: messages.expectedEmptyLineBefore('display'), 1201 | }, 1202 | { 1203 | description: '73', 1204 | code: ` 1205 | a { 1206 | height: 10px; 1207 | width: 10px; /* comment */ 1208 | display: none; 1209 | } 1210 | `, 1211 | fixed: ` 1212 | a { 1213 | height: 10px; 1214 | width: 10px; /* comment */ 1215 | 1216 | display: none; 1217 | } 1218 | `, 1219 | message: messages.expectedEmptyLineBefore('display'), 1220 | }, 1221 | { 1222 | description: '73.1', 1223 | code: ` 1224 | a { 1225 | 1226 | display: none; 1227 | } 1228 | `, 1229 | fixed: ` 1230 | a { 1231 | display: none; 1232 | } 1233 | `, 1234 | message: messages.rejectedEmptyLineBefore('display'), 1235 | }, 1236 | ], 1237 | }); 1238 | 1239 | testRule({ 1240 | ruleName, 1241 | config: [ 1242 | [ 1243 | { 1244 | emptyLineBefore: 'always', 1245 | properties: ['display'], 1246 | }, 1247 | { 1248 | emptyLineBefore: 'always', 1249 | properties: ['border'], 1250 | }, 1251 | ], 1252 | ], 1253 | fix: true, 1254 | 1255 | accept: [ 1256 | { 1257 | description: '74', 1258 | code: ` 1259 | a { 1260 | display: none; 1261 | 1262 | border: none; 1263 | } 1264 | `, 1265 | }, 1266 | ], 1267 | 1268 | reject: [ 1269 | { 1270 | description: '75', 1271 | code: ` 1272 | a { 1273 | display: none; 1274 | border: none; 1275 | } 1276 | `, 1277 | fixed: ` 1278 | a { 1279 | display: none; 1280 | 1281 | border: none; 1282 | } 1283 | `, 1284 | message: messages.expectedEmptyLineBefore('border'), 1285 | }, 1286 | ], 1287 | }); 1288 | 1289 | testRule({ 1290 | ruleName, 1291 | config: [ 1292 | [ 1293 | { 1294 | emptyLineBefore: 'always', 1295 | properties: ['display'], 1296 | }, 1297 | { 1298 | properties: ['position'], 1299 | }, 1300 | ], 1301 | ], 1302 | fix: true, 1303 | 1304 | accept: [ 1305 | { 1306 | description: '76', 1307 | code: ` 1308 | a { 1309 | display: none; 1310 | 1311 | position: absolute; 1312 | } 1313 | `, 1314 | }, 1315 | { 1316 | description: '77', 1317 | code: ` 1318 | a { 1319 | display: none; 1320 | position: absolute; 1321 | } 1322 | `, 1323 | }, 1324 | ], 1325 | 1326 | reject: [], 1327 | }); 1328 | 1329 | testRule({ 1330 | ruleName, 1331 | config: [ 1332 | [ 1333 | { 1334 | emptyLineBefore: 'always', 1335 | properties: ['width', 'height'], 1336 | }, 1337 | { 1338 | emptyLineBefore: 'always', 1339 | properties: ['font-size', 'font-family'], 1340 | }, 1341 | { 1342 | emptyLineBefore: 'always', 1343 | properties: ['background-repeat'], 1344 | }, 1345 | ], 1346 | ], 1347 | fix: true, 1348 | 1349 | reject: [ 1350 | { 1351 | description: 'fix order and empty line before', 1352 | code: ` 1353 | a { 1354 | width: 100%; 1355 | font-size: 14px; 1356 | height: 100%; 1357 | font-family: "Arial", "Helvetica", sans-serif; 1358 | background-repeat: no-repeat; 1359 | } 1360 | `, 1361 | fixed: ` 1362 | a { 1363 | width: 100%; 1364 | height: 100%; 1365 | 1366 | font-size: 14px; 1367 | font-family: "Arial", "Helvetica", sans-serif; 1368 | 1369 | background-repeat: no-repeat; 1370 | } 1371 | `, 1372 | warnings: [ 1373 | { 1374 | message: messages.expected('height', 'font-size'), 1375 | }, 1376 | { 1377 | message: messages.expectedEmptyLineBefore('font-size'), 1378 | }, 1379 | { 1380 | message: messages.expectedEmptyLineBefore('height'), 1381 | }, 1382 | { 1383 | message: messages.expectedEmptyLineBefore('font-family'), 1384 | }, 1385 | { 1386 | message: messages.expectedEmptyLineBefore('background-repeat'), 1387 | }, 1388 | ], 1389 | }, 1390 | { 1391 | description: 'fix empty line before, order is fine', 1392 | code: ` 1393 | a { 1394 | width: 100%; 1395 | height: 100%; 1396 | font-size: 14px; 1397 | font-family: "Arial", "Helvetica", sans-serif; 1398 | background-repeat: no-repeat; 1399 | } 1400 | `, 1401 | fixed: ` 1402 | a { 1403 | width: 100%; 1404 | height: 100%; 1405 | 1406 | font-size: 14px; 1407 | font-family: "Arial", "Helvetica", sans-serif; 1408 | 1409 | background-repeat: no-repeat; 1410 | } 1411 | `, 1412 | warnings: [ 1413 | { 1414 | message: messages.expectedEmptyLineBefore('font-size'), 1415 | }, 1416 | { 1417 | message: messages.expectedEmptyLineBefore('background-repeat'), 1418 | }, 1419 | ], 1420 | }, 1421 | ], 1422 | }); 1423 | -------------------------------------------------------------------------------- /rules/properties-order/tests/empty-line-minimum-property-threshold.js: -------------------------------------------------------------------------------- 1 | import { rule } from '../index.js'; 2 | 3 | const { ruleName, messages } = rule; 4 | 5 | testRule({ 6 | ruleName, 7 | config: [ 8 | [ 9 | { 10 | emptyLineBefore: 'threshold', 11 | properties: ['display'], 12 | }, 13 | { 14 | emptyLineBefore: 'threshold', 15 | properties: ['position'], 16 | }, 17 | { 18 | emptyLineBefore: 'threshold', 19 | properties: ['border-bottom', 'font-style'], 20 | }, 21 | ], 22 | { 23 | emptyLineMinimumPropertyThreshold: 5, 24 | }, 25 | ], 26 | fix: true, 27 | 28 | accept: [ 29 | { 30 | description: '1', 31 | code: ` 32 | a { 33 | display: none; 34 | position: absolute; 35 | border-bottom: 1px solid red; 36 | font-style: italic; 37 | } 38 | `, 39 | }, 40 | { 41 | description: '2', 42 | code: ` 43 | a { 44 | display: none; 45 | position: absolute; 46 | font-style: italic; 47 | } 48 | `, 49 | }, 50 | { 51 | description: '3', 52 | code: ` 53 | a { 54 | display: none; 55 | font-style: italic; 56 | } 57 | `, 58 | }, 59 | { 60 | description: '4', 61 | code: ` 62 | a { 63 | position: absolute; 64 | border-bottom: 1px solid red; 65 | } 66 | `, 67 | }, 68 | { 69 | description: '5', 70 | code: ` 71 | a { 72 | display: none; 73 | border-bottom: 1px solid red; 74 | } 75 | `, 76 | }, 77 | { 78 | description: '6', 79 | code: ` 80 | a { 81 | display: none; /* comment */ 82 | position: absolute; 83 | } 84 | `, 85 | }, 86 | { 87 | description: '7', 88 | code: ` 89 | a { 90 | display: none; 91 | /* comment */ 92 | position: absolute; 93 | } 94 | `, 95 | }, 96 | { 97 | description: '8', 98 | code: ` 99 | a { 100 | /* comment */ 101 | display: none; 102 | position: absolute; 103 | } 104 | `, 105 | }, 106 | { 107 | description: '9', 108 | code: ` 109 | a { 110 | /* comment */ 111 | display: none; 112 | 113 | /* comment */ 114 | position: absolute; 115 | } 116 | `, 117 | }, 118 | { 119 | description: '12', 120 | code: ` 121 | a { 122 | --display: none; 123 | 124 | position: absolute; 125 | } 126 | `, 127 | }, 128 | { 129 | description: '13', 130 | code: ` 131 | a { 132 | --display: none; 133 | position: absolute; 134 | } 135 | `, 136 | }, 137 | { 138 | description: '13.1', 139 | code: ` 140 | a { 141 | $display: none; 142 | position: absolute; 143 | } 144 | `, 145 | }, 146 | { 147 | description: '13.2', 148 | code: ` 149 | a { 150 | $display: none; 151 | 152 | position: absolute; 153 | } 154 | `, 155 | }, 156 | { 157 | description: '13.3', 158 | code: ` 159 | a { 160 | position: absolute; 161 | $display: none; 162 | } 163 | `, 164 | }, 165 | { 166 | description: '13.4', 167 | code: ` 168 | a { 169 | position: absolute; 170 | 171 | $display: none; 172 | } 173 | `, 174 | }, 175 | { 176 | description: '14', 177 | code: ` 178 | a { 179 | display: 0; 180 | 181 | /* comment */ 182 | position: 0; 183 | 184 | /* comment */ 185 | border-bottom: 0; 186 | } 187 | `, 188 | }, 189 | ], 190 | 191 | reject: [ 192 | { 193 | description: '14', 194 | code: ` 195 | a { 196 | display: none; 197 | 198 | position: absolute; 199 | 200 | border-bottom: 1px solid red; 201 | font-style: italic; 202 | } 203 | `, 204 | fixed: ` 205 | a { 206 | display: none; 207 | position: absolute; 208 | border-bottom: 1px solid red; 209 | font-style: italic; 210 | } 211 | `, 212 | warnings: [ 213 | { 214 | message: messages.rejectedEmptyLineBefore('position'), 215 | }, 216 | { 217 | message: messages.rejectedEmptyLineBefore('border-bottom'), 218 | }, 219 | ], 220 | }, 221 | { 222 | description: '15', 223 | code: ` 224 | a { 225 | display: none; 226 | 227 | position: absolute; 228 | border-bottom: 1px solid red; 229 | font-style: italic; 230 | } 231 | `, 232 | fixed: ` 233 | a { 234 | display: none; 235 | position: absolute; 236 | border-bottom: 1px solid red; 237 | font-style: italic; 238 | } 239 | `, 240 | message: messages.rejectedEmptyLineBefore('position'), 241 | }, 242 | { 243 | description: '16', 244 | code: ` 245 | a { 246 | display: none; 247 | position: absolute; 248 | 249 | font-style: italic; 250 | } 251 | `, 252 | fixed: ` 253 | a { 254 | display: none; 255 | position: absolute; 256 | font-style: italic; 257 | } 258 | `, 259 | message: messages.rejectedEmptyLineBefore('font-style'), 260 | }, 261 | { 262 | description: '17', 263 | code: ` 264 | a { 265 | display: none; 266 | 267 | font-style: italic; 268 | } 269 | `, 270 | fixed: ` 271 | a { 272 | display: none; 273 | font-style: italic; 274 | } 275 | `, 276 | message: messages.rejectedEmptyLineBefore('font-style'), 277 | }, 278 | { 279 | description: '18', 280 | code: ` 281 | a { 282 | position: absolute; 283 | 284 | border-bottom: 1px solid red; 285 | } 286 | `, 287 | fixed: ` 288 | a { 289 | position: absolute; 290 | border-bottom: 1px solid red; 291 | } 292 | `, 293 | message: messages.rejectedEmptyLineBefore('border-bottom'), 294 | }, 295 | { 296 | description: '19', 297 | code: ` 298 | a { 299 | display: none; 300 | 301 | border-bottom: 1px solid red; 302 | } 303 | `, 304 | fixed: ` 305 | a { 306 | display: none; 307 | border-bottom: 1px solid red; 308 | } 309 | `, 310 | message: messages.rejectedEmptyLineBefore('border-bottom'), 311 | }, 312 | { 313 | description: '20', 314 | code: ` 315 | a { 316 | display: none; /* comment */ 317 | 318 | position: absolute; 319 | } 320 | `, 321 | fixed: ` 322 | a { 323 | display: none; /* comment */ 324 | position: absolute; 325 | } 326 | `, 327 | message: messages.rejectedEmptyLineBefore('position'), 328 | }, 329 | { 330 | description: '21', 331 | code: ` 332 | a { 333 | /* comment */ 334 | display: none; 335 | 336 | position: absolute; 337 | } 338 | `, 339 | fixed: ` 340 | a { 341 | /* comment */ 342 | display: none; 343 | position: absolute; 344 | } 345 | `, 346 | message: messages.rejectedEmptyLineBefore('position'), 347 | }, 348 | { 349 | description: '22', 350 | code: ` 351 | a { 352 | display: absolute; 353 | 354 | --num: 0; 355 | 356 | position: var(--num); 357 | 358 | border-bottom: var(--num); 359 | } 360 | `, 361 | fixed: ` 362 | a { 363 | display: absolute; 364 | 365 | --num: 0; 366 | 367 | position: var(--num); 368 | border-bottom: var(--num); 369 | } 370 | `, 371 | message: messages.rejectedEmptyLineBefore('border-bottom'), 372 | }, 373 | ], 374 | }); 375 | 376 | testRule({ 377 | ruleName, 378 | config: [ 379 | [ 380 | { 381 | emptyLineBefore: 'threshold', 382 | properties: ['width', 'height'], 383 | }, 384 | { 385 | emptyLineBefore: 'threshold', 386 | properties: ['font-size', 'font-family'], 387 | }, 388 | { 389 | emptyLineBefore: 'threshold', 390 | properties: ['background-repeat'], 391 | }, 392 | ], 393 | { 394 | emptyLineMinimumPropertyThreshold: 4, 395 | }, 396 | ], 397 | fix: true, 398 | 399 | reject: [ 400 | { 401 | description: 'fix order and empty line before', 402 | code: ` 403 | a { 404 | width: 100%; 405 | font-size: 14px; 406 | height: 100%; 407 | font-family: "Arial", "Helvetica", sans-serif; 408 | background-repeat: no-repeat; 409 | } 410 | `, 411 | fixed: ` 412 | a { 413 | width: 100%; 414 | height: 100%; 415 | 416 | font-size: 14px; 417 | font-family: "Arial", "Helvetica", sans-serif; 418 | 419 | background-repeat: no-repeat; 420 | } 421 | `, 422 | warnings: [ 423 | { 424 | message: messages.expected('height', 'font-size'), 425 | }, 426 | { 427 | message: messages.expectedEmptyLineBefore('font-size'), 428 | }, 429 | { 430 | message: messages.expectedEmptyLineBefore('height'), 431 | }, 432 | { 433 | message: messages.expectedEmptyLineBefore('font-family'), 434 | }, 435 | { 436 | message: messages.expectedEmptyLineBefore('background-repeat'), 437 | }, 438 | ], 439 | }, 440 | { 441 | description: 'fix empty line before, order is fine', 442 | code: ` 443 | a { 444 | width: 100%; 445 | height: 100%; 446 | font-size: 14px; 447 | font-family: "Arial", "Helvetica", sans-serif; 448 | background-repeat: no-repeat; 449 | } 450 | `, 451 | fixed: ` 452 | a { 453 | width: 100%; 454 | height: 100%; 455 | 456 | font-size: 14px; 457 | font-family: "Arial", "Helvetica", sans-serif; 458 | 459 | background-repeat: no-repeat; 460 | } 461 | `, 462 | warnings: [ 463 | { 464 | message: messages.expectedEmptyLineBefore('font-size'), 465 | }, 466 | { 467 | message: messages.expectedEmptyLineBefore('background-repeat'), 468 | }, 469 | ], 470 | }, 471 | ], 472 | }); 473 | 474 | // Ensure compatibility with emptyLineBeforeUnspecified 475 | testRule({ 476 | ruleName, 477 | config: [ 478 | ['height', 'width'], 479 | { 480 | unspecified: 'bottom', 481 | emptyLineBeforeUnspecified: 'threshold', 482 | emptyLineMinimumPropertyThreshold: 4, 483 | }, 484 | ], 485 | fix: true, 486 | 487 | accept: [ 488 | { 489 | description: 'emptyLineBeforeUnspecified-compat-1', 490 | code: ` 491 | a { 492 | height: 1px; 493 | width: 2px; 494 | color: blue; 495 | } 496 | `, 497 | }, 498 | { 499 | description: 'emptyLineBeforeUnspecified-compat-2', 500 | code: ` 501 | a { 502 | height: 1px; 503 | width: 2px; 504 | 505 | color: blue; 506 | transform: none; 507 | } 508 | `, 509 | }, 510 | ], 511 | 512 | reject: [ 513 | { 514 | description: 'emptyLineBeforeUnspecified-compat-3', 515 | code: ` 516 | a { 517 | height: 1px; 518 | width: 2px; 519 | color: blue; 520 | transform: none; 521 | } 522 | `, 523 | fixed: ` 524 | a { 525 | height: 1px; 526 | width: 2px; 527 | 528 | color: blue; 529 | transform: none; 530 | } 531 | `, 532 | message: messages.expectedEmptyLineBefore('color'), 533 | }, 534 | ], 535 | }); 536 | 537 | // Documented example verification 538 | testRule({ 539 | ruleName, 540 | config: [ 541 | [ 542 | { 543 | emptyLineBefore: 'threshold', 544 | properties: ['display'], 545 | }, 546 | { 547 | emptyLineBefore: 'threshold', 548 | properties: ['height', 'width'], 549 | }, 550 | { 551 | emptyLineBefore: 'always', 552 | properties: ['border'], 553 | }, 554 | { 555 | emptyLineBefore: 'never', 556 | properties: ['transform'], 557 | }, 558 | ], 559 | { 560 | emptyLineMinimumPropertyThreshold: 4, 561 | }, 562 | ], 563 | fix: true, 564 | 565 | accept: [ 566 | { 567 | description: 'example-accept-1', 568 | code: ` 569 | a { 570 | display: block; 571 | height: 1px; 572 | width: 2px; 573 | } 574 | `, 575 | }, 576 | { 577 | description: 'example-accept-2', 578 | code: ` 579 | a { 580 | display: block; 581 | height: 1px; 582 | 583 | border: 0; 584 | } 585 | `, 586 | }, 587 | { 588 | description: 'example-accept-3', 589 | code: ` 590 | a { 591 | display: block; 592 | 593 | height: 1px; 594 | width: 2px; 595 | 596 | border: 0; 597 | } 598 | `, 599 | }, 600 | { 601 | description: 'example-accept-4', 602 | code: ` 603 | a { 604 | display: block; 605 | 606 | height: 1px; 607 | width: 2px; 608 | 609 | border: 0; 610 | transform: none; 611 | } 612 | `, 613 | }, 614 | ], 615 | 616 | reject: [ 617 | { 618 | description: 'example-reject-1', 619 | code: ` 620 | a { 621 | display: block; 622 | 623 | height: 1px; 624 | width: 2px; 625 | } 626 | `, 627 | fixed: ` 628 | a { 629 | display: block; 630 | height: 1px; 631 | width: 2px; 632 | } 633 | `, 634 | message: messages.rejectedEmptyLineBefore('height'), 635 | }, 636 | { 637 | description: 'example-reject-2', 638 | code: ` 639 | a { 640 | display: block; 641 | 642 | height: 1px; 643 | border: 0; 644 | } 645 | `, 646 | fixed: ` 647 | a { 648 | display: block; 649 | height: 1px; 650 | 651 | border: 0; 652 | } 653 | `, 654 | 655 | warnings: [ 656 | { 657 | message: messages.rejectedEmptyLineBefore('height'), 658 | }, 659 | { 660 | message: messages.expectedEmptyLineBefore('border'), 661 | }, 662 | ], 663 | }, 664 | { 665 | description: 'example-reject-3', 666 | code: ` 667 | a { 668 | display: block; 669 | height: 1px; 670 | width: 2px; 671 | border: 0; 672 | } 673 | `, 674 | fixed: ` 675 | a { 676 | display: block; 677 | 678 | height: 1px; 679 | width: 2px; 680 | 681 | border: 0; 682 | } 683 | `, 684 | warnings: [ 685 | { 686 | message: messages.expectedEmptyLineBefore('height'), 687 | }, 688 | { 689 | message: messages.expectedEmptyLineBefore('border'), 690 | }, 691 | ], 692 | }, 693 | { 694 | description: 'example-reject-4', 695 | code: ` 696 | a { 697 | display: block; 698 | height: 1px; 699 | width: 2px; 700 | transform: none; 701 | } 702 | `, 703 | fixed: ` 704 | a { 705 | display: block; 706 | 707 | height: 1px; 708 | width: 2px; 709 | transform: none; 710 | } 711 | `, 712 | message: messages.expectedEmptyLineBefore('height'), 713 | }, 714 | ], 715 | }); 716 | 717 | // Verify mix of settings 718 | testRule({ 719 | ruleName, 720 | config: [ 721 | [ 722 | { 723 | emptyLineBefore: 'threshold', 724 | properties: ['display'], 725 | }, 726 | { 727 | emptyLineBefore: 'threshold', 728 | properties: ['height', 'width'], 729 | }, 730 | { 731 | emptyLineBefore: 'always', 732 | properties: ['border'], 733 | }, 734 | { 735 | emptyLineBefore: 'never', 736 | properties: ['transform'], 737 | }, 738 | ], 739 | { 740 | unspecified: 'bottom', 741 | emptyLineBeforeUnspecified: 'threshold', 742 | emptyLineMinimumPropertyThreshold: 4, 743 | }, 744 | ], 745 | fix: true, 746 | 747 | accept: [ 748 | { 749 | description: 'mixed-accept-1', 750 | code: ` 751 | a { 752 | display: block; 753 | height: 1px; 754 | width: 2px; 755 | } 756 | `, 757 | }, 758 | { 759 | description: 'mixed-accept-2', 760 | code: ` 761 | a { 762 | display: block; 763 | 764 | height: 1px; 765 | width: 2px; 766 | 767 | border: 0; 768 | } 769 | `, 770 | }, 771 | { 772 | description: 'mixed-accept-3', 773 | code: ` 774 | a { 775 | display: block; 776 | 777 | height: 1px; 778 | width: 2px; 779 | 780 | border: 0; 781 | transform: none; 782 | } 783 | `, 784 | }, 785 | { 786 | description: 'mixed-accept-4', 787 | code: ` 788 | a { 789 | display: block; 790 | height: 1px; 791 | 792 | border: 0; 793 | } 794 | `, 795 | }, 796 | { 797 | description: 'mixed-accept-5', 798 | code: ` 799 | a { 800 | border: 0; 801 | transform: none; 802 | color: blue; 803 | } 804 | `, 805 | }, 806 | { 807 | description: 'mixed-accept-6', 808 | code: ` 809 | a { 810 | display: block; 811 | 812 | height: 1px; 813 | width: 2px; 814 | 815 | border: 0; 816 | transform: none; 817 | 818 | color: blue; 819 | } 820 | `, 821 | }, 822 | ], 823 | 824 | reject: [ 825 | { 826 | description: 'mixed-reject-1', 827 | code: ` 828 | a { 829 | display: block; 830 | 831 | height: 1px; 832 | width: 2px; 833 | color: blue; 834 | } 835 | `, 836 | fixed: ` 837 | a { 838 | display: block; 839 | 840 | height: 1px; 841 | width: 2px; 842 | 843 | color: blue; 844 | } 845 | `, 846 | message: messages.expectedEmptyLineBefore('color'), 847 | }, 848 | { 849 | description: 'mixed-reject-2', 850 | code: ` 851 | a { 852 | display: block; 853 | 854 | height: 1px; 855 | width: 2px; 856 | border: 0; 857 | color: blue; 858 | } 859 | `, 860 | fixed: ` 861 | a { 862 | display: block; 863 | 864 | height: 1px; 865 | width: 2px; 866 | 867 | border: 0; 868 | 869 | color: blue; 870 | } 871 | `, 872 | warnings: [ 873 | { 874 | message: messages.expectedEmptyLineBefore('border'), 875 | }, 876 | { 877 | message: messages.expectedEmptyLineBefore('color'), 878 | }, 879 | ], 880 | }, 881 | { 882 | description: 'mixed-reject-3', 883 | code: ` 884 | a { 885 | display: block; 886 | 887 | height: 1px; 888 | width: 2px; 889 | border: 0; 890 | 891 | transform: none; 892 | color: blue; 893 | } 894 | `, 895 | fixed: ` 896 | a { 897 | display: block; 898 | 899 | height: 1px; 900 | width: 2px; 901 | 902 | border: 0; 903 | transform: none; 904 | 905 | color: blue; 906 | } 907 | `, 908 | warnings: [ 909 | { 910 | message: messages.expectedEmptyLineBefore('border'), 911 | }, 912 | { 913 | message: messages.rejectedEmptyLineBefore('transform'), 914 | }, 915 | { 916 | message: messages.expectedEmptyLineBefore('color'), 917 | }, 918 | ], 919 | }, 920 | ], 921 | }); 922 | -------------------------------------------------------------------------------- /rules/properties-order/tests/flat.js: -------------------------------------------------------------------------------- 1 | import { rule } from '../index.js'; 2 | 3 | const { ruleName, messages } = rule; 4 | 5 | testRule({ 6 | ruleName, 7 | config: [['my', 'transform', 'font-smoothing', 'top', 'transition', 'border', 'color']], 8 | fix: true, 9 | 10 | accept: [ 11 | { 12 | code: 'a { color: pink; }', 13 | }, 14 | { 15 | code: 'a { color: pink; color: red; }', 16 | }, 17 | { 18 | code: 'a { top: 0; color: pink; }', 19 | }, 20 | { 21 | code: 'a { -moz-transform: scale(1); -webkit-transform: scale(1); transform: scale(1); }', 22 | }, 23 | { 24 | code: 'a { -webkit-transform: scale(1); -moz-transform: scale(1); transform: scale(1); }', 25 | }, 26 | { 27 | code: 'a { -webkit-font-smoothing: antialiased; top: 0; color: pink; }', 28 | }, 29 | { 30 | code: 'a { top: 0; color: pink; width: 0; }', 31 | }, 32 | { 33 | code: 'a { top: 0; color: pink; width: 0; height: 0; }', 34 | }, 35 | { 36 | code: 'a { @media (min-width: 10px) { color: pink; } top: 0; }', 37 | description: 'media query nested in rule has its own ordering', 38 | }, 39 | { 40 | code: 'a { border: 1px solid; color: pink; }', 41 | }, 42 | { 43 | code: 'a { transition: none; border: 1px solid; }', 44 | }, 45 | { 46 | code: 'a { top: 0; color: pink; width: 0; height: 0; display: none; }', 47 | }, 48 | { 49 | code: 'a { top: 0; color: pink; display: none; width: 0; height: 0; }', 50 | }, 51 | { 52 | code: 'a { top: 0; Top: 0; color: pink; }', 53 | }, 54 | { 55 | code: 'a { Top: 0; top: 0; color: pink; }', 56 | }, 57 | ], 58 | 59 | reject: [ 60 | { 61 | code: 'a { color: pink; top: 0; }', 62 | fixed: 'a { top: 0; color: pink; }', 63 | message: messages.expected('top', 'color'), 64 | }, 65 | { 66 | code: 'a { Color: pink; top: 0; }', 67 | fixed: 'a { top: 0; Color: pink; }', 68 | message: messages.expected('top', 'Color'), 69 | }, 70 | { 71 | code: 'a { Top: 0; color: pink; top: 0; }', 72 | fixed: 'a { Top: 0; top: 0; color: pink; }', 73 | message: messages.expected('top', 'color'), 74 | }, 75 | { 76 | code: 'a { top: 0; transform: scale(1); color: pink; }', 77 | fixed: 'a { transform: scale(1); top: 0; color: pink; }', 78 | message: messages.expected('transform', 'top'), 79 | }, 80 | { 81 | code: 'a { -moz-transform: scale(1); transform: scale(1); -webkit-transform: scale(1); }', 82 | fixed: 'a { -moz-transform: scale(1); -webkit-transform: scale(1); transform: scale(1); }', 83 | message: messages.expected('-webkit-transform', 'transform'), 84 | }, 85 | { 86 | code: 'a { color: pink; -webkit-font-smoothing: antialiased; }', 87 | fixed: 'a { -webkit-font-smoothing: antialiased; color: pink; }', 88 | message: messages.expected('-webkit-font-smoothing', 'color'), 89 | }, 90 | { 91 | code: 'a { color: pink; border: 1px solid; }', 92 | fixed: 'a { border: 1px solid; color: pink; }', 93 | message: messages.expected('border', 'color'), 94 | }, 95 | { 96 | code: 'a { border: 1px solid; transition: "foo"; }', 97 | fixed: 'a { transition: "foo"; border: 1px solid; }', 98 | message: messages.expected('transition', 'border'), 99 | }, 100 | { 101 | code: 'a { @media (min-width: 10px) { color: pink; top: 0; } transform: scale(1); }', 102 | fixed: 'a { @media (min-width: 10px) { top: 0; color: pink; } transform: scale(1); }', 103 | description: 'media query nested in rule can violates its own ordering', 104 | message: messages.expected('top', 'color'), 105 | }, 106 | ], 107 | }); 108 | 109 | testRule({ 110 | ruleName, 111 | config: [['my', 'transform', 'font-smoothing', 'top', 'transition', 'border', 'color']], 112 | 113 | accept: [ 114 | { 115 | code: 'a { my-property: 2em; -webkit-font-smoothing: antialiased; }', 116 | }, 117 | { 118 | code: 'a { display: none; top: 0; color: pink; width: 0; height: 0; }', 119 | }, 120 | ], 121 | }); 122 | 123 | testRule({ 124 | ruleName, 125 | config: [ 126 | [ 127 | 'padding', 128 | 'padding-top', 129 | 'padding-right', 130 | 'padding-left', 131 | 'border', 132 | 'border-top', 133 | 'border-right', 134 | 'color', 135 | ], 136 | ], 137 | fix: true, 138 | 139 | accept: [ 140 | { 141 | code: 'a { padding: 1px; color: pink; }', 142 | }, 143 | { 144 | code: 'a { padding-top: 1px; color: pink; }', 145 | }, 146 | { 147 | code: 'a { padding-left: 1px; color: pink; }', 148 | }, 149 | { 150 | code: 'a { padding-top: 1px; padding-right: 0; color: pink; }', 151 | }, 152 | { 153 | code: 'a { border: 1px solid #fff; border-right: 2px solid #fff; border-right-color: #000; }', 154 | }, 155 | { 156 | code: 'a { border: 1px solid #fff; border-top: none; border-right-color: #000; }', 157 | }, 158 | ], 159 | 160 | reject: [ 161 | { 162 | code: 'a { color: pink; padding: 1px; }', 163 | fixed: 'a { padding: 1px; color: pink; }', 164 | message: messages.expected('padding', 'color'), 165 | }, 166 | { 167 | code: 'a { color: pink; padding-top: 1px; }', 168 | fixed: 'a { padding-top: 1px; color: pink; }', 169 | message: messages.expected('padding-top', 'color'), 170 | }, 171 | { 172 | code: 'a { padding-right: 1px; padding-top: 0; color: pink; }', 173 | fixed: 'a { padding-top: 0; padding-right: 1px; color: pink; }', 174 | message: messages.expected('padding-top', 'padding-right'), 175 | }, 176 | ], 177 | }); 178 | 179 | testRule({ 180 | ruleName, 181 | config: [ 182 | [ 183 | 'padding', 184 | 'padding-top', 185 | 'padding-right', 186 | 'padding-left', 187 | 'border', 188 | 'border-top', 189 | 'border-right', 190 | 'color', 191 | ], 192 | ], 193 | 194 | accept: [ 195 | { 196 | code: 'a { padding-bottom: 0; padding-top: 1px; padding-right: 0; padding-left: 0; color: pink; }', 197 | }, 198 | { 199 | code: 'a { padding: 1px; padding-bottom: 0; padding-left: 0; color: pink; }', 200 | }, 201 | ], 202 | }); 203 | 204 | testRule({ 205 | ruleName, 206 | config: [ 207 | ['height', 'color'], 208 | { 209 | unspecified: 'top', 210 | }, 211 | ], 212 | fix: true, 213 | 214 | accept: [ 215 | { 216 | code: 'a { top: 0; height: 1px; color: pink; }', 217 | }, 218 | { 219 | code: 'a { bottom: 0; top: 0; }', 220 | }, 221 | ], 222 | 223 | reject: [ 224 | { 225 | code: 'a { height: 1px; top: 0; }', 226 | fixed: 'a { top: 0; height: 1px; }', 227 | message: messages.expected('top', 'height'), 228 | }, 229 | { 230 | code: 'a { color: 1px; top: 0; }', 231 | fixed: 'a { top: 0; color: 1px; }', 232 | message: messages.expected('top', 'color'), 233 | }, 234 | ], 235 | }); 236 | 237 | testRule({ 238 | ruleName, 239 | config: [ 240 | ['height', 'color'], 241 | { 242 | unspecified: 'bottom', 243 | }, 244 | ], 245 | fix: true, 246 | 247 | accept: [ 248 | { 249 | code: 'a { height: 1px; color: pink; bottom: 0; }', 250 | }, 251 | { 252 | code: 'a { bottom: 0; top: 0; }', 253 | }, 254 | ], 255 | 256 | reject: [ 257 | { 258 | code: 'a { bottom: 0; height: 1px; }', 259 | fixed: 'a { height: 1px; bottom: 0; }', 260 | message: messages.expected('height', 'bottom'), 261 | }, 262 | { 263 | code: 'a { bottom: 0; color: 1px; }', 264 | fixed: 'a { color: 1px; bottom: 0; }', 265 | message: messages.expected('color', 'bottom'), 266 | }, 267 | ], 268 | }); 269 | 270 | testRule({ 271 | ruleName, 272 | config: [ 273 | ['all', 'compose'], 274 | { 275 | unspecified: 'bottomAlphabetical', 276 | }, 277 | ], 278 | fix: true, 279 | 280 | accept: [ 281 | { 282 | code: 'a { all: initial; compose: b; }', 283 | }, 284 | { 285 | code: 'a { bottom: 0; top: 0; }', 286 | }, 287 | { 288 | code: 'a { all: initial; compose: b; bottom: 0; top: 0; }', 289 | }, 290 | ], 291 | 292 | reject: [ 293 | { 294 | code: 'a { align-items: flex-end; all: initial; }', 295 | fixed: 'a { all: initial; align-items: flex-end; }', 296 | message: messages.expected('all', 'align-items'), 297 | }, 298 | { 299 | code: 'a { compose: b; top: 0; bottom: 0; }', 300 | fixed: 'a { compose: b; bottom: 0; top: 0; }', 301 | message: messages.expected('bottom', 'top'), 302 | }, 303 | ], 304 | }); 305 | 306 | testRule({ 307 | ruleName, 308 | config: [['left', 'margin']], 309 | fix: true, 310 | 311 | accept: [ 312 | { 313 | code: '.foo { left: 0; color: pink; margin: 0; }', 314 | }, 315 | ], 316 | 317 | reject: [ 318 | { 319 | description: `report incorrect order if there're properties with undefined order`, 320 | code: '.foo { margin: 0; color: pink; left: 0; }', 321 | fixed: '.foo { left: 0; margin: 0; color: pink; }', 322 | message: messages.expected('left', 'margin'), 323 | }, 324 | ], 325 | }); 326 | 327 | testRule({ 328 | ruleName, 329 | config: [['top', 'color']], 330 | customSyntax: 'postcss-styled-syntax', 331 | fix: true, 332 | 333 | accept: [ 334 | { 335 | code: ` 336 | const Component = styled.div\` 337 | top: 0; 338 | color: tomato; 339 | \`; 340 | `, 341 | }, 342 | { 343 | code: ` 344 | const Component = styled.div\` 345 | top: 0; 346 | \${props => props.great && 'color: red;'} 347 | color: tomato; 348 | \`; 349 | `, 350 | }, 351 | { 352 | code: ` 353 | const Component = styled.div\` 354 | top: 0; 355 | \${props => props.great && 'color: red;'} 356 | color: tomato; 357 | 358 | a { 359 | top: 0; 360 | color: tomato; 361 | } 362 | \`; 363 | `, 364 | }, 365 | { 366 | code: ` 367 | const Component = styled.div\` 368 | top: 0; 369 | color: tomato; 370 | 371 | a { 372 | top: 0; 373 | \${props => props.great && 'color: red;'} 374 | color: tomato; 375 | } 376 | \`; 377 | `, 378 | }, 379 | ], 380 | 381 | reject: [ 382 | { 383 | code: ` 384 | const Component = styled.div\` 385 | color: tomato; 386 | top: 0; 387 | \`; 388 | `, 389 | fixed: ` 390 | const Component = styled.div\` 391 | top: 0; 392 | color: tomato; 393 | \`; 394 | `, 395 | message: messages.expected('top', 'color'), 396 | }, 397 | { 398 | code: ` 399 | const Component = styled.div\` 400 | color: tomato; 401 | \${props => props.great && 'color: red;'} 402 | top: 0; 403 | \`; 404 | `, 405 | unfixable: true, 406 | message: messages.expected('top', 'color'), 407 | }, 408 | { 409 | code: ` 410 | const Component = styled.div\` 411 | color: tomato; 412 | \${props => props.great && 'color: red;'} 413 | top: 0; 414 | 415 | a { 416 | top: 0; 417 | color: tomato; 418 | } 419 | \`; 420 | `, 421 | unfixable: true, 422 | message: messages.expected('top', 'color'), 423 | }, 424 | { 425 | code: ` 426 | const Component = styled.div\` 427 | top: 0; 428 | color: tomato; 429 | 430 | a { 431 | color: tomato; 432 | \${props => props.great && 'color: red;'} 433 | top: 0; 434 | } 435 | \`; 436 | `, 437 | unfixable: true, 438 | message: messages.expected('top', 'color'), 439 | }, 440 | { 441 | code: ` 442 | const Component = styled.div\` 443 | \${() => css\` 444 | color: tomato; 445 | top: 0; 446 | \`} 447 | \`; 448 | `, 449 | fixed: ` 450 | const Component = styled.div\` 451 | \${() => css\` 452 | top: 0; 453 | color: tomato; 454 | \`} 455 | \`; 456 | `, 457 | description: 'sort inside css helper', 458 | message: messages.expected('top', 'color'), 459 | }, 460 | ], 461 | }); 462 | 463 | testRule({ 464 | ruleName, 465 | config: [['top', 'color']], 466 | customSyntax: 'postcss-html', 467 | fix: true, 468 | 469 | accept: [ 470 | { 471 | code: ` 472 | 473 | 474 | 475 | 481 | 482 | 483 | 484 | 485 | `, 486 | }, 487 | { 488 | code: ` 489 | 490 | 491 | 492 |
493 | 494 | 495 | `, 496 | }, 497 | ], 498 | 499 | reject: [ 500 | { 501 | code: ` 502 | 503 | 504 | 505 | 511 | 512 | 513 | 514 | 515 | `, 516 | fixed: ` 517 | 518 | 519 | 520 | 526 | 527 | 528 | 529 | 530 | `, 531 | message: messages.expected('top', 'color'), 532 | }, 533 | { 534 | code: ` 535 | 536 | 537 | 538 |
539 | 540 | 541 | `, 542 | fixed: ` 543 | 544 | 545 | 546 |
547 | 548 | 549 | `, 550 | message: messages.expected('top', 'color'), 551 | }, 552 | ], 553 | }); 554 | 555 | testRule({ 556 | ruleName, 557 | config: [['width', 'height']], 558 | fix: true, 559 | 560 | reject: [ 561 | { 562 | description: 'Fix should apply, when disable comments were used', 563 | code: ` 564 | /* stylelint-disable order/properties-order */ 565 | /* stylelint-enable order/properties-order */ 566 | 567 | a { 568 | height: 0; 569 | width: 0; 570 | } 571 | `, 572 | fixed: ` 573 | /* stylelint-disable order/properties-order */ 574 | /* stylelint-enable order/properties-order */ 575 | 576 | a { 577 | width: 0; 578 | height: 0; 579 | } 580 | `, 581 | message: messages.expected('width', 'height'), 582 | }, 583 | ], 584 | }); 585 | -------------------------------------------------------------------------------- /rules/properties-order/tests/grouped-flexible.js: -------------------------------------------------------------------------------- 1 | import { rule } from '../index.js'; 2 | 3 | const { ruleName, messages } = rule; 4 | 5 | testRule({ 6 | ruleName, 7 | config: [ 8 | [ 9 | 'height', 10 | 'width', 11 | { 12 | order: 'flexible', 13 | properties: ['color', 'font-size', 'font-weight'], 14 | }, 15 | ], 16 | ], 17 | fix: true, 18 | 19 | accept: [ 20 | { 21 | code: 'a { height: 1px; width: 2px; color: pink; font-size: 2px; font-weight: bold; }', 22 | }, 23 | { 24 | code: 'a { height: 1px; width: 2px; font-size: 2px; color: pink; font-weight: bold; }', 25 | }, 26 | { 27 | code: 'a { height: 1px; width: 2px; font-size: 2px; font-weight: bold; color: pink; }', 28 | }, 29 | { 30 | code: 'a { height: 1px; width: 2px; font-weight: bold; font-size: 2px; color: pink; }', 31 | }, 32 | { 33 | code: 'a { height: 10px; background: orange; }', 34 | description: 'unspecified after groupless specified', 35 | }, 36 | { 37 | code: 'a { font-weight: bold; background: orange; }', 38 | description: 'unspecified after grouped specified', 39 | }, 40 | { 41 | code: 'a { background: orange; height: 10px; }', 42 | description: 'unspecified before groupless specified', 43 | }, 44 | { 45 | code: 'a { background: orange; font-weight: bold; }', 46 | description: 'unspecified before grouped specified', 47 | }, 48 | { 49 | code: 'a { height: 1px; width: 2px; color: pink; font-size: 2px; font-weight: bold; }', 50 | }, 51 | { 52 | code: 'a { height: 10px; background: orange; }', 53 | description: 'unspecified after groupless specified', 54 | }, 55 | { 56 | code: 'a { font-weight: bold; background: orange; }', 57 | description: 'unspecified after grouped specified', 58 | }, 59 | ], 60 | 61 | reject: [ 62 | { 63 | code: 'a { height: 1px; font-weight: bold; width: 2px; }', 64 | fixed: 'a { height: 1px; width: 2px; font-weight: bold; }', 65 | message: messages.expected('width', 'font-weight'), 66 | line: 1, 67 | column: 37, 68 | }, 69 | { 70 | code: 'a { font-weight: bold; height: 1px; width: 2px; }', 71 | fixed: 'a { height: 1px; width: 2px; font-weight: bold; }', 72 | message: messages.expected('height', 'font-weight'), 73 | line: 1, 74 | column: 24, 75 | }, 76 | { 77 | code: 'a { width: 2px; height: 1px; font-weight: bold; }', 78 | fixed: 'a { height: 1px; width: 2px; font-weight: bold; }', 79 | message: messages.expected('height', 'width'), 80 | line: 1, 81 | column: 17, 82 | }, 83 | { 84 | code: 'a { height: 1px; color: pink; width: 2px; font-weight: bold; }', 85 | fixed: 'a { height: 1px; width: 2px; color: pink; font-weight: bold; }', 86 | message: messages.expected('width', 'color'), 87 | line: 1, 88 | column: 31, 89 | }, 90 | ], 91 | }); 92 | 93 | // Also test with groupName 94 | testRule({ 95 | ruleName, 96 | config: [ 97 | [ 98 | { 99 | groupName: 'font', 100 | order: 'flexible', 101 | properties: ['font-size', 'font-weight'], 102 | }, 103 | 'height', 104 | 'width', 105 | ], 106 | ], 107 | 108 | accept: [ 109 | { 110 | code: 'a { font-size: 2px; font-weight: bold; height: 1px; width: 2px; }', 111 | }, 112 | ], 113 | reject: [ 114 | { 115 | code: 'a { height: 1px; font-weight: bold; }', 116 | message: messages.expected('font-weight', 'height', 'font'), 117 | line: 1, 118 | column: 18, 119 | }, 120 | ], 121 | }); 122 | 123 | testRule({ 124 | ruleName, 125 | config: [ 126 | [ 127 | { 128 | order: 'flexible', 129 | properties: ['width', 'height'], 130 | }, 131 | { 132 | order: 'flexible', 133 | properties: ['color', 'font-size', 'font-weight'], 134 | }, 135 | ], 136 | ], 137 | fix: true, 138 | 139 | accept: [ 140 | { 141 | code: 'a { height: 1px; width: 2px; color: pink; font-size: 2px; font-weight: bold; }', 142 | }, 143 | { 144 | code: 'a { width: 2px; height: 1px; font-size: 2px; color: pink; font-weight: bold; }', 145 | }, 146 | { 147 | code: 'a { height: 1px; width: 2px; font-size: 2px; font-weight: bold; color: pink; }', 148 | }, 149 | { 150 | code: 'a { width: 2px; height: 1px; font-weight: bold; font-size: 2px; color: pink; }', 151 | }, 152 | ], 153 | 154 | reject: [ 155 | { 156 | code: 'a { height: 1px; font-weight: bold; width: 2px; }', 157 | fixed: 'a { width: 2px; height: 1px; font-weight: bold; }', 158 | message: messages.expected('width', 'font-weight'), 159 | line: 1, 160 | column: 37, 161 | }, 162 | { 163 | code: 'a { font-weight: bold; height: 1px; width: 2px; }', 164 | fixed: 'a { width: 2px; height: 1px; font-weight: bold; }', 165 | message: messages.expected('height', 'font-weight'), 166 | line: 1, 167 | column: 24, 168 | }, 169 | { 170 | code: 'a { height: 1px; color: pink; width: 2px; font-weight: bold; }', 171 | fixed: 'a { width: 2px; height: 1px; color: pink; font-weight: bold; }', 172 | message: messages.expected('width', 'color'), 173 | line: 1, 174 | column: 31, 175 | }, 176 | ], 177 | }); 178 | 179 | // Also test with groupName 180 | testRule({ 181 | ruleName, 182 | config: [ 183 | [ 184 | { 185 | groupName: 'dimensions', 186 | order: 'flexible', 187 | properties: ['width', 'height'], 188 | }, 189 | { 190 | groupName: 'font', 191 | order: 'flexible', 192 | properties: ['color', 'font-size', 'font-weight'], 193 | }, 194 | ], 195 | ], 196 | 197 | accept: [ 198 | { 199 | code: 'a { height: 1px; width: 2px; color: pink; font-size: 2px; font-weight: bold; }', 200 | }, 201 | ], 202 | reject: [ 203 | { 204 | code: 'a { height: 1px; font-weight: bold; width: 2px; }', 205 | fixed: 'a { width: 2px; height: 1px; font-weight: bold; }', 206 | message: messages.expected('width', 'font-weight', 'dimensions'), 207 | line: 1, 208 | column: 37, 209 | }, 210 | ], 211 | }); 212 | -------------------------------------------------------------------------------- /rules/properties-order/tests/grouped-strict.js: -------------------------------------------------------------------------------- 1 | import { rule } from '../index.js'; 2 | 3 | const { ruleName, messages } = rule; 4 | 5 | testRule({ 6 | ruleName, 7 | config: [ 8 | [ 9 | 'height', 10 | 'width', 11 | { 12 | properties: ['color', 'font-size', 'font-weight'], 13 | }, 14 | ], 15 | ], 16 | 17 | accept: [ 18 | { 19 | code: 'a { background: orange; height: 10px; }', 20 | description: 'unspecified before groupless specified', 21 | }, 22 | { 23 | code: 'a { background: orange; font-weight: bold; }', 24 | description: 'unspecified before grouped specified', 25 | }, 26 | ], 27 | }); 28 | 29 | // Also test with groupName 30 | testRule({ 31 | ruleName, 32 | config: [ 33 | [ 34 | { 35 | groupName: 'font', 36 | properties: ['font-size', 'font-weight'], 37 | }, 38 | 'height', 39 | 'width', 40 | ], 41 | ], 42 | 43 | accept: [ 44 | { 45 | code: 'a { font-size: 2px; font-weight: bold; height: 1px; width: 2px; }', 46 | }, 47 | ], 48 | reject: [ 49 | { 50 | code: 'a { height: 1px; font-weight: bold; }', 51 | message: messages.expected('font-weight', 'height', 'font'), 52 | line: 1, 53 | column: 18, 54 | }, 55 | { 56 | code: 'a { font-weight: bold; font-size: 2px; height: 1px; }', 57 | message: messages.expected('font-size', 'font-weight', 'font'), 58 | line: 1, 59 | column: 24, 60 | }, 61 | ], 62 | }); 63 | 64 | testRule({ 65 | ruleName, 66 | config: [ 67 | [ 68 | 'height', 69 | 'width', 70 | { 71 | properties: ['color', 'font-size', 'font-weight'], 72 | }, 73 | ], 74 | ], 75 | fix: true, 76 | 77 | accept: [ 78 | { 79 | code: 'a { height: 1px; width: 2px; color: pink; font-size: 2px; font-weight: bold; }', 80 | }, 81 | { 82 | code: 'a { height: 10px; background: orange; }', 83 | description: 'unspecified after groupless specified', 84 | }, 85 | { 86 | code: 'a { font-weight: bold; background: orange; }', 87 | description: 'unspecified after grouped specified', 88 | }, 89 | ], 90 | 91 | reject: [ 92 | { 93 | code: 'a { width: 2px; color: pink; font-size: 2px; font-weight: bold; height: 1px; }', 94 | fixed: 'a { height: 1px; width: 2px; color: pink; font-size: 2px; font-weight: bold; }', 95 | message: messages.expected('height', 'font-weight'), 96 | }, 97 | { 98 | code: 'a { height: 1px; color: pink; width: 2px; font-size: 2px; font-weight: bold; }', 99 | fixed: 'a { height: 1px; width: 2px; color: pink; font-size: 2px; font-weight: bold; }', 100 | message: messages.expected('width', 'color'), 101 | }, 102 | { 103 | code: 'a { height: 1px; width: 2px; font-size: 2px; color: pink; font-weight: bold; }', 104 | fixed: 'a { height: 1px; width: 2px; color: pink; font-size: 2px; font-weight: bold; }', 105 | message: messages.expected('color', 'font-size'), 106 | }, 107 | { 108 | code: 'a { height: 1px; width: 2px; font-size: 2px; font-weight: bold; color: pink; }', 109 | fixed: 'a { height: 1px; width: 2px; color: pink; font-size: 2px; font-weight: bold; }', 110 | message: messages.expected('color', 'font-weight'), 111 | }, 112 | { 113 | code: 'a { height: 1px; width: 2px; color: pink; font-weight: bold; font-size: 2px; }', 114 | fixed: 'a { height: 1px; width: 2px; color: pink; font-size: 2px; font-weight: bold; }', 115 | message: messages.expected('font-size', 'font-weight'), 116 | }, 117 | ], 118 | }); 119 | 120 | testRule({ 121 | ruleName, 122 | config: [ 123 | [ 124 | { 125 | properties: ['width', 'height'], 126 | }, 127 | { 128 | properties: ['color', 'font-size', 'font-weight'], 129 | }, 130 | ], 131 | ], 132 | fix: true, 133 | 134 | accept: [ 135 | { 136 | code: 'a { width: 2px; height: 1px; color: pink; font-size: 2px; font-weight: bold; }', 137 | }, 138 | ], 139 | 140 | reject: [ 141 | { 142 | code: 'a { width: 2px; color: pink; font-size: 2px; font-weight: bold; height: 1px; }', 143 | fixed: 'a { width: 2px; height: 1px; color: pink; font-size: 2px; font-weight: bold; }', 144 | message: messages.expected('height', 'font-weight'), 145 | }, 146 | { 147 | code: 'a { height: 1px; color: pink; width: 2px; font-size: 2px; font-weight: bold; }', 148 | fixed: 'a { width: 2px; height: 1px; color: pink; font-size: 2px; font-weight: bold; }', 149 | message: messages.expected('width', 'color'), 150 | }, 151 | { 152 | code: 'a { width: 2px; height: 1px; font-size: 2px; color: pink; font-weight: bold; }', 153 | fixed: 'a { width: 2px; height: 1px; color: pink; font-size: 2px; font-weight: bold; }', 154 | message: messages.expected('color', 'font-size'), 155 | }, 156 | { 157 | code: 'a { width: 2px; height: 1px; font-size: 2px; font-weight: bold; color: pink; }', 158 | fixed: 'a { width: 2px; height: 1px; color: pink; font-size: 2px; font-weight: bold; }', 159 | message: messages.expected('color', 'font-weight'), 160 | }, 161 | { 162 | code: 'a { width: 2px; height: 1px; color: pink; font-weight: bold; font-size: 2px; }', 163 | fixed: 'a { width: 2px; height: 1px; color: pink; font-size: 2px; font-weight: bold; }', 164 | message: messages.expected('font-size', 'font-weight'), 165 | }, 166 | ], 167 | }); 168 | -------------------------------------------------------------------------------- /rules/properties-order/tests/no-empty-line-between.js: -------------------------------------------------------------------------------- 1 | import { rule } from '../index.js'; 2 | 3 | const { ruleName, messages } = rule; 4 | 5 | testRule({ 6 | ruleName, 7 | config: [ 8 | [ 9 | { 10 | noEmptyLineBetween: true, 11 | properties: ['display', 'vertical-align', 'content'], 12 | }, 13 | { 14 | noEmptyLineBetween: true, 15 | properties: ['position', 'top', 'bottom'], 16 | }, 17 | ], 18 | ], 19 | fix: true, 20 | 21 | accept: [ 22 | { 23 | code: ` 24 | a { 25 | display: block; 26 | vertical-align: middle; 27 | content: ""; 28 | position: absolute; 29 | top: 0; 30 | bottom: 0; 31 | } 32 | `, 33 | }, 34 | { 35 | code: ` 36 | a { 37 | display: block; 38 | vertical-align: middle; 39 | content: ""; 40 | 41 | position: absolute; 42 | top: 0; 43 | bottom: 0; 44 | } 45 | `, 46 | }, 47 | { 48 | code: ` 49 | a { 50 | vertical-align: middle; 51 | 52 | top: 0; 53 | bottom: 0; 54 | } 55 | `, 56 | }, 57 | ], 58 | 59 | reject: [ 60 | { 61 | code: ` 62 | a { 63 | display: block; 64 | 65 | vertical-align: middle; 66 | content: ""; 67 | } 68 | `, 69 | fixed: ` 70 | a { 71 | display: block; 72 | vertical-align: middle; 73 | content: ""; 74 | } 75 | `, 76 | message: messages.rejectedEmptyLineBefore('vertical-align'), 77 | }, 78 | { 79 | code: ` 80 | a { 81 | display: block; 82 | vertical-align: middle; 83 | 84 | 85 | content: ""; 86 | } 87 | `, 88 | fixed: ` 89 | a { 90 | display: block; 91 | vertical-align: middle; 92 | content: ""; 93 | } 94 | `, 95 | message: messages.rejectedEmptyLineBefore('content'), 96 | }, 97 | ], 98 | }); 99 | 100 | testRule({ 101 | ruleName, 102 | config: [ 103 | [ 104 | { 105 | noEmptyLineBetween: true, 106 | properties: ['display', 'vertical-align', 'content'], 107 | }, 108 | { 109 | noEmptyLineBetween: true, 110 | properties: ['position', 'top', 'bottom'], 111 | }, 112 | ], 113 | ], 114 | customSyntax: 'postcss-styled-syntax', 115 | fix: true, 116 | 117 | accept: [ 118 | { 119 | code: ` 120 | const Component = styled.div\` 121 | display: block; 122 | vertical-align: middle; 123 | content: ""; 124 | position: absolute; 125 | top: 0; 126 | bottom: 0; 127 | \`; 128 | `, 129 | }, 130 | ], 131 | 132 | reject: [ 133 | { 134 | code: ` 135 | const Component = styled.div\` 136 | display: block; 137 | 138 | vertical-align: middle; 139 | content: ""; 140 | \`; 141 | `, 142 | fixed: ` 143 | const Component = styled.div\` 144 | display: block; 145 | vertical-align: middle; 146 | content: ""; 147 | \`; 148 | `, 149 | message: messages.rejectedEmptyLineBefore('vertical-align'), 150 | }, 151 | ], 152 | }); 153 | 154 | testRule({ 155 | ruleName, 156 | config: [ 157 | [ 158 | { 159 | emptyLineBefore: 'always', 160 | noEmptyLineBetween: true, 161 | properties: ['display', 'vertical-align', 'content'], 162 | }, 163 | { 164 | emptyLineBefore: 'always', 165 | noEmptyLineBetween: true, 166 | properties: ['position', 'top', 'bottom'], 167 | }, 168 | ], 169 | ], 170 | fix: true, 171 | 172 | accept: [ 173 | { 174 | code: ` 175 | a { 176 | display: block; 177 | vertical-align: middle; 178 | content: ""; 179 | 180 | position: absolute; 181 | top: 0; 182 | bottom: 0; 183 | } 184 | `, 185 | }, 186 | { 187 | code: ` 188 | a { 189 | vertical-align: middle; 190 | 191 | top: 0; 192 | bottom: 0; 193 | } 194 | `, 195 | }, 196 | ], 197 | 198 | reject: [ 199 | { 200 | code: ` 201 | a { 202 | display: block; 203 | 204 | vertical-align: middle; 205 | content: ""; 206 | } 207 | `, 208 | fixed: ` 209 | a { 210 | display: block; 211 | vertical-align: middle; 212 | content: ""; 213 | } 214 | `, 215 | message: messages.rejectedEmptyLineBefore('vertical-align'), 216 | }, 217 | ], 218 | }); 219 | 220 | testRule({ 221 | ruleName, 222 | config: [ 223 | [ 224 | { 225 | emptyLineBefore: 'always', 226 | noEmptyLineBetween: true, 227 | order: 'flexible', 228 | properties: ['height', 'width'], 229 | }, 230 | { 231 | emptyLineBefore: 'always', 232 | noEmptyLineBetween: true, 233 | order: 'flexible', 234 | properties: ['font-size', 'font-weight'], 235 | }, 236 | ], 237 | ], 238 | fix: true, 239 | 240 | accept: [ 241 | { 242 | code: ` 243 | a { 244 | height: 1px; 245 | width: 2px; 246 | 247 | font-size: 2px; 248 | font-weight: bold; 249 | } 250 | `, 251 | }, 252 | { 253 | code: ` 254 | a { 255 | height: 1px; 256 | width: 2px; 257 | 258 | font-weight: bold; 259 | font-size: 2px; 260 | } 261 | `, 262 | }, 263 | ], 264 | 265 | reject: [ 266 | { 267 | code: ` 268 | a { 269 | height: 1px; 270 | width: 2px; 271 | 272 | font-weight: bold; 273 | 274 | font-size: 2px; 275 | } 276 | `, 277 | fixed: ` 278 | a { 279 | height: 1px; 280 | width: 2px; 281 | 282 | font-weight: bold; 283 | font-size: 2px; 284 | } 285 | `, 286 | message: messages.rejectedEmptyLineBefore('font-size'), 287 | }, 288 | { 289 | code: ` 290 | a { 291 | height: 1px; 292 | width: 2px; 293 | 294 | font-size: 2px; 295 | 296 | font-weight: bold; 297 | } 298 | `, 299 | fixed: ` 300 | a { 301 | height: 1px; 302 | width: 2px; 303 | 304 | font-size: 2px; 305 | font-weight: bold; 306 | } 307 | `, 308 | message: messages.rejectedEmptyLineBefore('font-weight'), 309 | }, 310 | ], 311 | }); 312 | -------------------------------------------------------------------------------- /rules/properties-order/tests/removeEmptyLineBefore.test.js: -------------------------------------------------------------------------------- 1 | import { removeEmptyLinesBefore } from '../removeEmptyLinesBefore.js'; 2 | import postcss from 'postcss'; 3 | 4 | function removeEmptyLine(css, lineEnding) { 5 | const root = postcss.parse(css); 6 | 7 | removeEmptyLinesBefore(root.nodes[1], lineEnding); 8 | 9 | return root.toString(); 10 | } 11 | 12 | describe('removeEmptyLinesBefore', () => { 13 | it('removes single newline from the newline at the beginning', () => { 14 | expect(removeEmptyLine('a {}\n\n b{}', '\n')).toBe('a {}\n b{}'); 15 | }); 16 | 17 | it('removes single newline from newline at the beginning with CRLF', () => { 18 | expect(removeEmptyLine('a {}\r\n\r\n b{}', '\r\n')).toBe('a {}\r\n b{}'); 19 | }); 20 | 21 | it('removes single newline from newline at the end', () => { 22 | expect(removeEmptyLine('a {}\t\n\nb{}', '\n')).toBe('a {}\t\nb{}'); 23 | }); 24 | 25 | it('removes single newline from newline at the end with CRLF', () => { 26 | expect(removeEmptyLine('a {}\t\r\n\r\nb{}', '\r\n')).toBe('a {}\t\r\nb{}'); 27 | }); 28 | 29 | it('removes single newline from newline in the middle', () => { 30 | expect(removeEmptyLine('a {} \n\n\tb{}', '\n')).toBe('a {} \n\tb{}'); 31 | }); 32 | 33 | it('removes single newline to newline in the middle with CRLF', () => { 34 | expect(removeEmptyLine('a {} \r\n\r\n\tb{}', '\r\n')).toBe('a {} \r\n\tb{}'); 35 | }); 36 | 37 | it('removes two newlines if there are three newlines', () => { 38 | expect(removeEmptyLine('a {}\n\n\n b{}', '\n')).toBe('a {}\n b{}'); 39 | }); 40 | 41 | it('removes two newlines if there are three newlines with CRLF', () => { 42 | expect(removeEmptyLine('a {}\r\n\r\n\r\n b{}', '\r\n')).toBe('a {}\r\n b{}'); 43 | }); 44 | 45 | it('removes three newlines if there are four newlines', () => { 46 | expect(removeEmptyLine('a {}\n\n\n\n b{}', '\n')).toBe('a {}\n b{}'); 47 | }); 48 | 49 | it('removes three newlines if there are four newlines with CRLF', () => { 50 | expect(removeEmptyLine('a {}\r\n\r\n\r\n\r\n b{}', '\r\n')).toBe('a {}\r\n b{}'); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /rules/properties-order/tests/report-when-not-fixed.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import { rule } from '../index.js'; 3 | 4 | const { ruleName } = rule; 5 | 6 | test(`show warning if --fix enabled, but it didn't fix`, () => { 7 | const code = ` 8 | const Component = styled.div\` 9 | color: tomato; 10 | \${interpolation} 11 | top: 0; 12 | \`; 13 | `; 14 | 15 | const stylelintConfig = { 16 | plugins: ['./'], 17 | rules: { 18 | [ruleName]: [['top', 'color']], 19 | }, 20 | }; 21 | 22 | const options = { 23 | code, 24 | config: stylelintConfig, 25 | customSyntax: 'postcss-styled-syntax', 26 | fix: true, 27 | }; 28 | 29 | return stylelint.lint(options).then((output) => { 30 | expect(output.results[0].warnings.length).toBe(1); 31 | 32 | const fixedCode = getOutputCss(output); 33 | 34 | expect(fixedCode).toBe(code); 35 | }); 36 | }); 37 | 38 | test(`don't show warning if --fix enabled, and it fixed`, () => { 39 | const code = ` 40 | const Component = styled.div\` 41 | color: tomato; 42 | top: 0; 43 | \`; 44 | `; 45 | 46 | const expectedCode = ` 47 | const Component = styled.div\` 48 | top: 0; 49 | color: tomato; 50 | \`; 51 | `; 52 | 53 | const stylelintConfig = { 54 | plugins: ['./'], 55 | rules: { 56 | [ruleName]: [['top', 'color']], 57 | }, 58 | }; 59 | 60 | const options = { 61 | code, 62 | config: stylelintConfig, 63 | customSyntax: 'postcss-styled-syntax', 64 | fix: true, 65 | }; 66 | 67 | return stylelint.lint(options).then((output) => { 68 | expect(output.results[0].warnings.length).toBe(0); 69 | 70 | const fixedCode = getOutputCss(output); 71 | 72 | expect(fixedCode).toBe(expectedCode); 73 | }); 74 | }); 75 | 76 | function getOutputCss(output) { 77 | const result = output.results[0]._postcssResult; 78 | const css = result.root.toString(result.opts.syntax); 79 | 80 | return css; 81 | } 82 | -------------------------------------------------------------------------------- /rules/properties-order/tests/validate-options.js: -------------------------------------------------------------------------------- 1 | import { rule } from '../index.js'; 2 | 3 | const { ruleName } = rule; 4 | 5 | testConfig({ 6 | ruleName, 7 | description: 'valid groups with emptyLineBefore: "always"', 8 | valid: true, 9 | config: [ 10 | { 11 | emptyLineBefore: 'always', 12 | order: 'flexible', 13 | properties: ['border-bottom', 'font-style'], 14 | }, 15 | { 16 | emptyLineBefore: 'never', 17 | order: 'strict', 18 | properties: ['position'], 19 | }, 20 | { 21 | emptyLineBefore: 'always', 22 | order: 'strict', 23 | properties: ['display'], 24 | }, 25 | ], 26 | }); 27 | 28 | testConfig({ 29 | ruleName, 30 | description: 'valid group and declaration', 31 | valid: true, 32 | config: [ 33 | 'height', 34 | 'width', 35 | { 36 | emptyLineBefore: 'always', 37 | order: 'strict', 38 | properties: ['display'], 39 | }, 40 | ], 41 | }); 42 | 43 | testConfig({ 44 | ruleName, 45 | description: 'valid groups (one without emptyLineBefore)', 46 | valid: true, 47 | config: [ 48 | { 49 | properties: ['display'], 50 | }, 51 | { 52 | emptyLineBefore: 'always', 53 | order: 'strict', 54 | properties: ['border'], 55 | }, 56 | ], 57 | }); 58 | 59 | testConfig({ 60 | ruleName, 61 | description: 'empty properties', 62 | valid: true, 63 | config: [ 64 | { 65 | emptyLineBefore: 'always', 66 | properties: [], 67 | }, 68 | ], 69 | }); 70 | 71 | testConfig({ 72 | ruleName, 73 | description: 'noEmptyLineBetween', 74 | valid: true, 75 | config: [ 76 | { 77 | emptyLineBefore: 'always', 78 | noEmptyLineBetween: true, 79 | properties: [], 80 | }, 81 | ], 82 | }); 83 | 84 | testConfig({ 85 | ruleName, 86 | description: 'invalid noEmptyLineBetween', 87 | valid: false, 88 | config: [ 89 | { 90 | noEmptyLineBetween: 'true', 91 | properties: [], 92 | }, 93 | ], 94 | message: `Invalid option "[{"noEmptyLineBetween":"true","properties":[]}]" for rule "${ruleName}"`, 95 | }); 96 | 97 | testConfig({ 98 | ruleName, 99 | description: 'invalid emptyLineBefore', 100 | valid: false, 101 | config: [ 102 | { 103 | emptyLineBefore: true, 104 | order: 'flexible', 105 | properties: ['border-bottom', 'font-style'], 106 | }, 107 | ], 108 | message: `Invalid option "[{"emptyLineBefore":true,"order":"flexible","properties":["border-bottom","font-style"]}]" for rule "${ruleName}"`, 109 | }); 110 | 111 | testConfig({ 112 | ruleName, 113 | description: 'properties should be an array', 114 | valid: false, 115 | config: [ 116 | { 117 | emptyLineBefore: 'always', 118 | order: 'flexible', 119 | properties: null, 120 | }, 121 | ], 122 | message: `Invalid option "[{"emptyLineBefore":"always","order":"flexible","properties":null}]" for rule "${ruleName}"`, 123 | }); 124 | -------------------------------------------------------------------------------- /rules/properties-order/validatePrimaryOption.js: -------------------------------------------------------------------------------- 1 | import { isBoolean, isString, isObject } from '../../utils/validateType.js'; 2 | 3 | export function validatePrimaryOption(actualOptions) { 4 | // Begin checking array options 5 | if (!Array.isArray(actualOptions)) { 6 | return false; 7 | } 8 | 9 | // Every item in the array must be a string or an object 10 | // with a "properties" property 11 | if ( 12 | !actualOptions.every((item) => { 13 | if (isString(item)) { 14 | return true; 15 | } 16 | 17 | return isObject(item) && item.properties !== undefined; 18 | }) 19 | ) { 20 | return false; 21 | } 22 | 23 | const objectItems = actualOptions.filter(isObject); 24 | 25 | // Every object-item's "properties" should be an array with no items, or with strings 26 | if ( 27 | !objectItems.every((item) => { 28 | if (!Array.isArray(item.properties)) { 29 | return false; 30 | } 31 | 32 | return item.properties.every((property) => isString(property)); 33 | }) 34 | ) { 35 | return false; 36 | } 37 | 38 | // Every object-item's "emptyLineBefore" must be "always" or "never" 39 | if ( 40 | !objectItems.every((item) => { 41 | if (item.emptyLineBefore === undefined) { 42 | return true; 43 | } 44 | 45 | return ['always', 'never', 'threshold'].includes(item.emptyLineBefore); 46 | }) 47 | ) { 48 | return false; 49 | } 50 | 51 | // Every object-item's "noEmptyLineBetween" must be a boolean 52 | if ( 53 | !objectItems.every((item) => { 54 | if (item.noEmptyLineBetween === undefined) { 55 | return true; 56 | } 57 | 58 | return isBoolean(item.noEmptyLineBetween); 59 | }) 60 | ) { 61 | return false; 62 | } 63 | 64 | return true; 65 | } 66 | -------------------------------------------------------------------------------- /utils/__tests__/isShorthand.test.js: -------------------------------------------------------------------------------- 1 | import { isShorthand } from '../isShorthand.js'; 2 | 3 | test('margin is shorthand for margin-top', () => { 4 | expect(isShorthand('margin', 'margin-top')).toBe(true); 5 | }); 6 | 7 | test('margin-top is not shorthand for margin', () => { 8 | expect(isShorthand('margin-top', 'margin')).toBe(false); 9 | }); 10 | 11 | test('margin-block is shorthand for margin-top', () => { 12 | expect(isShorthand('margin-block', 'margin-top')).toBe(true); 13 | }); 14 | 15 | test('margin-top is not shorthand for margin-block', () => { 16 | expect(isShorthand('margin-top', 'margin-block')).toBe(false); 17 | }); 18 | 19 | test('border-inline is shorthand for border-top-color', () => { 20 | expect(isShorthand('border-inline', 'border-top-color')).toBe(true); 21 | }); 22 | 23 | test('border-top-color is not shorthand for border-inline', () => { 24 | expect(isShorthand('border-top-color', 'border-inline')).toBe(false); 25 | }); 26 | -------------------------------------------------------------------------------- /utils/__tests__/vendor.test.js: -------------------------------------------------------------------------------- 1 | import { prefix, unprefixed } from '../vendor.js'; 2 | 3 | const VALUE = '-1px -1px 1px rgba(0, 0, 0, 0.2) inset'; 4 | 5 | it('returns prefix', () => { 6 | expect(prefix('-moz-color')).toBe('-moz-'); 7 | expect(prefix('color')).toBe(''); 8 | expect(prefix(VALUE)).toBe(''); 9 | }); 10 | 11 | it('returns unprefixed version', () => { 12 | expect(unprefixed('-moz-color')).toBe('color'); 13 | expect(unprefixed('color')).toBe('color'); 14 | expect(unprefixed(VALUE)).toEqual(VALUE); 15 | }); 16 | -------------------------------------------------------------------------------- /utils/checkAlphabeticalOrder.js: -------------------------------------------------------------------------------- 1 | import { isShorthand } from './isShorthand.js'; 2 | import * as vendor from './vendor.js'; 3 | 4 | export function checkAlphabeticalOrder(firstPropData, secondPropData) { 5 | let firstPropName = firstPropData.name.toLowerCase(); 6 | let secondPropName = secondPropData.name.toLowerCase(); 7 | let firstPropUnprefixedName = firstPropData.unprefixedName.toLowerCase(); 8 | let secondPropUnprefixedName = secondPropData.unprefixedName.toLowerCase(); 9 | 10 | // OK if the first is shorthand for the second: 11 | if (isShorthand(firstPropUnprefixedName, secondPropUnprefixedName)) { 12 | return true; 13 | } 14 | 15 | // Not OK if the second is shorthand for the first: 16 | if (isShorthand(secondPropUnprefixedName, firstPropUnprefixedName)) { 17 | return false; 18 | } 19 | 20 | // If unprefixed prop names are the same, compare the prefixed versions 21 | if (firstPropUnprefixedName === secondPropUnprefixedName) { 22 | // If first property has no prefix and second property has prefix 23 | if (!vendor.prefix(firstPropName).length && vendor.prefix(secondPropName).length) { 24 | return false; 25 | } 26 | 27 | return true; 28 | } 29 | 30 | return firstPropUnprefixedName < secondPropUnprefixedName; 31 | } 32 | -------------------------------------------------------------------------------- /utils/getContainingNode.js: -------------------------------------------------------------------------------- 1 | export function getContainingNode(node) { 2 | if (node.type === 'rule' || node.type === 'atrule') { 3 | return node; 4 | } 5 | 6 | // postcss-styled-syntax: declarations are children of Root node 7 | if (node.parent?.type === 'root' && node.parent?.raws.isRuleLike) { 8 | return node.parent; 9 | } 10 | 11 | // @stylelint/postcss-css-in-js: declarations are children of Root node 12 | if (node.parent?.document?.nodes?.some((item) => item.type === 'root')) { 13 | return node.parent; 14 | } 15 | 16 | return node; 17 | } 18 | -------------------------------------------------------------------------------- /utils/isAtVariable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check whether a property is a @-variable (Less) 3 | */ 4 | 5 | export function isAtVariable(node) { 6 | return node.type === 'atrule' && node.variable; 7 | } 8 | -------------------------------------------------------------------------------- /utils/isCustomProperty.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check whether a property is a custom one 3 | * 4 | * @param {string} property 5 | * @return {boolean} If `true`, property is a custom one 6 | */ 7 | 8 | export function isCustomProperty(property) { 9 | return property.startsWith('--'); 10 | } 11 | -------------------------------------------------------------------------------- /utils/isDollarVariable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check whether a property is a $-variable 3 | * 4 | * @param {string} property 5 | * @return {boolean} If `true`, property is a $-variable 6 | */ 7 | 8 | export function isDollarVariable(property) { 9 | return property.startsWith('$'); 10 | } 11 | -------------------------------------------------------------------------------- /utils/isLessMixin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check whether a property is a Less mixin 3 | */ 4 | 5 | export function isLessMixin(node) { 6 | return node.type === 'atrule' && node.mixin; 7 | } 8 | -------------------------------------------------------------------------------- /utils/isProperty.js: -------------------------------------------------------------------------------- 1 | // Check whether a property is a CSS property 2 | import { isCustomProperty } from './isCustomProperty.js'; 3 | import { isStandardSyntaxProperty } from './isStandardSyntaxProperty.js'; 4 | 5 | export function isProperty(node) { 6 | return ( 7 | node.type === 'decl' && isStandardSyntaxProperty(node.prop) && !isCustomProperty(node.prop) 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /utils/isRuleWithNodes.js: -------------------------------------------------------------------------------- 1 | export function isRuleWithNodes(node) { 2 | return node.nodes && node.nodes.length; 3 | } 4 | -------------------------------------------------------------------------------- /utils/isShorthand.js: -------------------------------------------------------------------------------- 1 | import { shorthandData } from './shorthandData.js'; 2 | 3 | export function isShorthand(a, b) { 4 | if (!shorthandData[a]) { 5 | return false; 6 | } 7 | 8 | if (shorthandData[a].includes(b)) { 9 | return true; 10 | } 11 | 12 | for (const longhand of shorthandData[a]) { 13 | if (isShorthand(longhand, b)) { 14 | return true; 15 | } 16 | } 17 | 18 | return false; 19 | } 20 | -------------------------------------------------------------------------------- /utils/isStandardSyntaxProperty.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check whether a property is standard 3 | * 4 | * @param {string} property 5 | * @return {boolean} If `true`, the property is standard 6 | */ 7 | 8 | export function isStandardSyntaxProperty(property) { 9 | // SCSS var (e.g. $var: x), list (e.g. $list: (x)) or map (e.g. $map: (key:value)) 10 | if (property.startsWith('$')) { 11 | return false; 12 | } 13 | 14 | // Less var (e.g. @var: x) 15 | if (property.startsWith('@')) { 16 | return false; 17 | } 18 | 19 | // SCSS or Less interpolation 20 | if (/#{.+?}|@{.+?}|\$\(.+?\)/.test(property)) { 21 | return false; 22 | } 23 | 24 | return true; 25 | } 26 | -------------------------------------------------------------------------------- /utils/namespace.js: -------------------------------------------------------------------------------- 1 | const prefix = 'order'; 2 | 3 | export function namespace(ruleName) { 4 | return `${prefix}/${ruleName}`; 5 | } 6 | -------------------------------------------------------------------------------- /utils/shorthandData.js: -------------------------------------------------------------------------------- 1 | // See https://github.com/stylelint/stylelint/blob/10.1.0/lib/reference/shorthandData.js 2 | // More properties were added in addition to the file above 3 | export const shorthandData = { 4 | margin: [ 5 | 'margin-top', 6 | 'margin-bottom', 7 | 'margin-left', 8 | 'margin-right', 9 | 'margin-block', 10 | 'margin-inline', 11 | ], 12 | 'margin-block': ['margin-block-start', 'margin-block-end'], 13 | 'margin-block-start': ['margin-top', 'margin-bottom', 'margin-left', 'margin-right'], 14 | 'margin-block-end': ['margin-top', 'margin-bottom', 'margin-left', 'margin-right'], 15 | 'margin-inline': ['margin-inline-start', 'margin-inline-end'], 16 | 'margin-inline-start': ['margin-top', 'margin-bottom', 'margin-left', 'margin-right'], 17 | 'margin-inline-end': ['margin-top', 'margin-bottom', 'margin-left', 'margin-right'], 18 | padding: [ 19 | 'padding-top', 20 | 'padding-bottom', 21 | 'padding-left', 22 | 'padding-right', 23 | 'padding-block', 24 | 'padding-block-start', 25 | 'padding-block-end', 26 | 'padding-inline', 27 | 'padding-inline-start', 28 | 'padding-inline-end', 29 | ], 30 | 'padding-block': [ 31 | 'padding-block-start', 32 | 'padding-block-end', 33 | 'padding-top', 34 | 'padding-bottom', 35 | 'padding-left', 36 | 'padding-right', 37 | ], 38 | 'padding-block-start': ['padding-top', 'padding-bottom', 'padding-left', 'padding-right'], 39 | 'padding-block-end': ['padding-top', 'padding-bottom', 'padding-left', 'padding-right'], 40 | 'padding-inline': [ 41 | 'padding-inline-start', 42 | 'padding-inline-end', 43 | 'padding-top', 44 | 'padding-bottom', 45 | 'padding-left', 46 | 'padding-right', 47 | ], 48 | 'padding-inline-start': ['padding-top', 'padding-bottom', 'padding-left', 'padding-right'], 49 | 'padding-inline-end': ['padding-top', 'padding-bottom', 'padding-left', 'padding-right'], 50 | background: [ 51 | 'background-image', 52 | 'background-size', 53 | 'background-position', 54 | 'background-repeat', 55 | 'background-origin', 56 | 'background-clip', 57 | 'background-attachment', 58 | 'background-color', 59 | ], 60 | font: [ 61 | 'font-style', 62 | 'font-variant', 63 | 'font-weight', 64 | 'font-stretch', 65 | 'font-size', 66 | 'font-family', 67 | 'line-height', 68 | ], 69 | border: [ 70 | 'border-inline', 71 | 'border-block', 72 | 'border-top', 73 | 'border-bottom', 74 | 'border-left', 75 | 'border-right', 76 | 'border-width', 77 | 'border-style', 78 | 'border-color', 79 | ], 80 | 'border-inline': [ 81 | 'border-inline-start', 82 | 'border-inline-end', 83 | 'border-inline-width', 84 | 'border-inline-style', 85 | 'border-inline-color', 86 | ], 87 | 'border-inline-width': ['border-inline-start-width', 'border-inline-end-width'], 88 | 'border-inline-style': ['border-inline-start-style', 'border-inline-end-style'], 89 | 'border-inline-color': ['border-inline-start-color', 'border-inline-end-color'], 90 | 'border-inline-start': [ 91 | 'border-inline-start-width', 92 | 'border-inline-start-style', 93 | 'border-inline-start-color', 94 | 95 | 'border-top', 96 | 'border-bottom', 97 | 'border-left', 98 | 'border-right', 99 | ], 100 | 'border-inline-start-width': [ 101 | 'border-top-width', 102 | 'border-bottom-width', 103 | 'border-left-width', 104 | 'border-right-width', 105 | ], 106 | 'border-inline-start-style': [ 107 | 'border-top-style', 108 | 'border-bottom-style', 109 | 'border-left-style', 110 | 'border-right-style', 111 | ], 112 | 'border-inline-start-color': [ 113 | 'border-top-color', 114 | 'border-bottom-color', 115 | 'border-left-color', 116 | 'border-right-color', 117 | ], 118 | 'border-inline-end': [ 119 | 'border-inline-end-width', 120 | 'border-inline-end-style', 121 | 'border-inline-end-color', 122 | 123 | 'border-top', 124 | 'border-bottom', 125 | 'border-left', 126 | 'border-right', 127 | ], 128 | 'border-inline-end-width': [ 129 | 'border-top-width', 130 | 'border-bottom-width', 131 | 'border-left-width', 132 | 'border-right-width', 133 | ], 134 | 'border-inline-end-style': [ 135 | 'border-top-style', 136 | 'border-bottom-style', 137 | 'border-left-style', 138 | 'border-right-style', 139 | ], 140 | 'border-inline-end-color': [ 141 | 'border-top-color', 142 | 'border-bottom-color', 143 | 'border-left-color', 144 | 'border-right-color', 145 | ], 146 | 'border-block': [ 147 | 'border-block-start', 148 | 'border-block-end', 149 | 'border-block-width', 150 | 'border-block-style', 151 | 'border-block-color', 152 | ], 153 | 'border-block-width': ['border-block-start-width', 'border-block-end-width'], 154 | 'border-block-style': ['border-block-start-style', 'border-block-end-style'], 155 | 'border-block-color': ['border-block-start-color', 'border-block-end-color'], 156 | 'border-block-start': [ 157 | 'border-block-start-width', 158 | 'border-block-start-style', 159 | 'border-block-start-color', 160 | 161 | 'border-top', 162 | 'border-bottom', 163 | 'border-left', 164 | 'border-right', 165 | ], 166 | 'border-block-start-width': [ 167 | 'border-top-width', 168 | 'border-bottom-width', 169 | 'border-left-width', 170 | 'border-right-width', 171 | ], 172 | 'border-block-start-style': [ 173 | 'border-top-style', 174 | 'border-bottom-style', 175 | 'border-left-style', 176 | 'border-right-style', 177 | ], 178 | 'border-block-start-color': [ 179 | 'border-top-color', 180 | 'border-bottom-color', 181 | 'border-left-color', 182 | 'border-right-color', 183 | ], 184 | 'border-block-end': [ 185 | 'border-block-end-width', 186 | 'border-block-end-style', 187 | 'border-block-end-color', 188 | 189 | 'border-top', 190 | 'border-bottom', 191 | 'border-left', 192 | 'border-right', 193 | ], 194 | 'border-block-end-width': [ 195 | 'border-top-width', 196 | 'border-bottom-width', 197 | 'border-left-width', 198 | 'border-right-width', 199 | ], 200 | 'border-block-end-style': [ 201 | 'border-top-style', 202 | 'border-bottom-style', 203 | 'border-left-style', 204 | 'border-right-style', 205 | ], 206 | 'border-block-end-color': [ 207 | 'border-top-color', 208 | 'border-bottom-color', 209 | 'border-left-color', 210 | 'border-right-color', 211 | ], 212 | 213 | 'border-top': ['border-top-width', 'border-top-style', 'border-top-color'], 214 | 'border-bottom': ['border-bottom-width', 'border-bottom-style', 'border-bottom-color'], 215 | 'border-left': ['border-left-width', 'border-left-style', 'border-left-color'], 216 | 'border-right': ['border-right-width', 'border-right-style', 'border-right-color'], 217 | 'border-width': [ 218 | 'border-top-width', 219 | 'border-bottom-width', 220 | 'border-left-width', 221 | 'border-right-width', 222 | ], 223 | 'border-style': [ 224 | 'border-top-style', 225 | 'border-bottom-style', 226 | 'border-left-style', 227 | 'border-right-style', 228 | ], 229 | 'border-color': [ 230 | 'border-top-color', 231 | 'border-bottom-color', 232 | 'border-left-color', 233 | 'border-right-color', 234 | ], 235 | 'border-image': [ 236 | 'border-image-source', 237 | 'border-image-slice', 238 | 'border-image-width', 239 | 'border-image-outset', 240 | 'border-image-repeat', 241 | ], 242 | 'border-radius': [ 243 | 'border-top-right-radius', 244 | 'border-top-left-radius', 245 | 'border-bottom-right-radius', 246 | 'border-bottom-left-radius', 247 | ], 248 | 'list-style': ['list-style-type', 'list-style-position', 'list-style-image'], 249 | transition: [ 250 | 'transition-delay', 251 | 'transition-duration', 252 | 'transition-property', 253 | 'transition-timing-function', 254 | ], 255 | animation: [ 256 | 'animation-name', 257 | 'animation-duration', 258 | 'animation-timing-function', 259 | 'animation-delay', 260 | 'animation-iteration-count', 261 | 'animation-direction', 262 | 'animation-fill-mode', 263 | 'animation-play-state', 264 | ], 265 | 'column-rule': ['column-rule-width', 'column-rule-style', 'column-rule-color'], 266 | columns: ['column-width', 'column-count'], 267 | flex: ['flex-grow', 'flex-shrink', 'flex-basis'], 268 | 'flex-flow': ['flex-direction', 'flex-wrap'], 269 | grid: [ 270 | 'grid-template-rows', 271 | 'grid-template-columns', 272 | 'grid-template-areas', 273 | 'grid-auto-rows', 274 | 'grid-auto-columns', 275 | 'grid-auto-flow', 276 | 'grid-column-gap', 277 | 'grid-row-gap', 278 | ], 279 | 'grid-area': ['grid-row-start', 'grid-column-start', 'grid-row-end', 'grid-column-end'], 280 | 'grid-column': ['grid-column-start', 'grid-column-end'], 281 | 'grid-gap': ['grid-row-gap', 'grid-column-gap'], 282 | 'grid-row': ['grid-row-start', 'grid-row-end'], 283 | 'grid-template': ['grid-template-columns', 'grid-template-rows', 'grid-template-areas'], 284 | offset: ['offset-anchor', 'offset-distance', 'offset-path', 'offset-position', 'offset-rotate'], 285 | outline: ['outline-color', 'outline-style', 'outline-width'], 286 | overflow: ['overflow-block', 'overflow-inline', 'overflow-x', 'overflow-y'], 287 | 'overflow-block': ['overflow-x', 'overflow-y'], 288 | 'overflow-inline': ['overflow-x', 'overflow-y'], 289 | 'overscroll-behavior': [ 290 | 'overscroll-behavior-x', 291 | 'overscroll-behavior-y', 292 | 'overscroll-behavior-block', 293 | 'overscroll-behavior-inline', 294 | ], 295 | 'overscroll-behavior-block': ['overscroll-behavior-x', 'overscroll-behavior-y'], 296 | 'overscroll-behavior-inline': ['overscroll-behavior-x', 'overscroll-behavior-y'], 297 | 'text-decoration': ['text-decoration-color', 'text-decoration-style', 'text-decoration-line'], 298 | 'text-emphasis': ['text-emphasis-style', 'text-emphasis-color'], 299 | mask: [ 300 | 'mask-image', 301 | 'mask-mode', 302 | 'mask-position', 303 | 'mask-size', 304 | 'mask-repeat', 305 | 'mask-origin', 306 | 'mask-clip', 307 | 'mask-composite', 308 | ], 309 | 'mask-border': [ 310 | 'mask-border-mode', 311 | 'mask-border-outset', 312 | 'mask-border-repeat', 313 | 'mask-border-slice', 314 | 'mask-border-source', 315 | 'mask-border-width', 316 | ], 317 | inset: ['top', 'right', 'bottom', 'left', 'inset-block', 'inset-inline'], 318 | 'inset-block': ['inset-block-end', 'inset-block-start'], 319 | 'inset-inline': ['inset-inline-end', 'inset-inline-start'], 320 | 'inset-block-start': ['top', 'bottom', 'left', 'right'], 321 | 'inset-block-end': ['top', 'bottom', 'left', 'right'], 322 | 'inset-inline-start': ['top', 'bottom', 'left', 'right'], 323 | 'inset-inline-end': ['top', 'bottom', 'left', 'right'], 324 | }; 325 | -------------------------------------------------------------------------------- /utils/validateType.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if the value is a boolean or a Boolean object. 3 | * @param {any} value 4 | * @returns {boolean} 5 | */ 6 | export function isBoolean(value) { 7 | return typeof value === 'boolean' || value instanceof Boolean; 8 | } 9 | 10 | /** 11 | * Checks if the value is a number or a Number object. 12 | * @param {any} value 13 | * @returns {boolean} 14 | */ 15 | export function isNumber(value) { 16 | return typeof value === 'number' || value instanceof Number; 17 | } 18 | 19 | /** 20 | * Checks if the value is a RegExp object. 21 | * @param {any} value 22 | * @returns {boolean} 23 | */ 24 | export function isRegExp(value) { 25 | return value instanceof RegExp; 26 | } 27 | 28 | /** 29 | * Checks if the value is a string or a String object. 30 | * @param {any} value 31 | * @returns {boolean} 32 | */ 33 | export function isString(value) { 34 | return typeof value === 'string' || value instanceof String; 35 | } 36 | 37 | /** 38 | * Checks if the value is an object. 39 | * @param {any} value 40 | * @returns {boolean} 41 | */ 42 | export function isObject(value) { 43 | return typeof value === 'object' && value !== null; 44 | } 45 | -------------------------------------------------------------------------------- /utils/vendor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains helpers for working with vendor prefixes. 3 | * 4 | * Copied from https://github.com/postcss/postcss/commit/777c55b5d2a10605313a4972888f4f32005f5ac2 5 | * 6 | */ 7 | 8 | /** 9 | * Returns the vendor prefix extracted from an input string. 10 | * 11 | * @param {string} prop String with or without vendor prefix. 12 | * 13 | * @return {string} vendor prefix or empty string 14 | * 15 | * @example 16 | * vendor.prefix('-moz-tab-size') //=> '-moz-' 17 | * vendor.prefix('tab-size') //=> '' 18 | */ 19 | export function prefix(prop) { 20 | let match = prop.match(/^(-\w+-)/); 21 | 22 | if (match) { 23 | return match[0]; 24 | } 25 | 26 | return ''; 27 | } 28 | 29 | /** 30 | * Returns the input string stripped of its vendor prefix. 31 | * 32 | * @param {string} prop String with or without vendor prefix. 33 | * 34 | * @return {string} String name without vendor prefixes. 35 | * 36 | * @example 37 | * vendor.unprefixed('-moz-tab-size') //=> 'tab-size' 38 | */ 39 | export function unprefixed(prop) { 40 | return prop.replace(/^-\w+-/, ''); 41 | } 42 | --------------------------------------------------------------------------------