├── .editorconfig ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── REPORT_A_BUG.yml │ ├── REPORT_A_DOCS_ISSUE.yml │ └── REQUEST_A_FEATURE.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── nodejs.yml ├── .gitignore ├── .prettierrc.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── eslint.config.mjs ├── lib ├── index.js ├── rules │ ├── alpha-values │ │ ├── README.md │ │ ├── index.js │ │ └── index.test.js │ ├── border-widths │ │ ├── README.md │ │ ├── index.js │ │ └── index.test.js │ ├── font-sizes │ │ ├── README.md │ │ ├── index.js │ │ └── index.test.js │ ├── font-weights │ │ ├── README.md │ │ ├── index.js │ │ └── index.test.js │ ├── letter-spacings │ │ ├── README.md │ │ ├── index.js │ │ └── index.test.js │ ├── line-heights │ │ ├── README.md │ │ ├── index.js │ │ └── index.test.js │ ├── radii │ │ ├── README.md │ │ ├── index.js │ │ └── index.test.js │ ├── sizes │ │ ├── README.md │ │ ├── index.js │ │ └── index.test.js │ ├── space │ │ ├── README.md │ │ ├── index.js │ │ └── index.test.js │ ├── word-spacings │ │ ├── README.md │ │ ├── index.js │ │ └── index.test.js │ └── z-indices │ │ ├── README.md │ │ ├── index.js │ │ └── index.test.js └── utils │ ├── createRuleMessages.js │ ├── findScaleByUnit.js │ ├── findScaleByUnit.test.js │ ├── getClosest.js │ ├── getValue.js │ ├── hasNumericScale.js │ ├── hasNumericScale.test.js │ ├── hasObjectWithNumericArray.js │ ├── hasScalesWithUnits.js │ ├── isIgnoredFunctionArgument.js │ ├── isLineHeight.js │ ├── isOnNumericScale.js │ ├── isSlash.js │ └── setValue.js ├── package-lock.json └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jeddy3 2 | * @digitaljohn 3 | * @nattog -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | 3 | You should adhere to GitHub's [Acceptable Use](https://help.github.com/articles/github-terms-of-service/#c-acceptable-use) policy. 4 | 5 | You can use GitHub's [grievance process](https://github.com/contact/report-abuse) to report breaches of this policy. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/REPORT_A_BUG.yml: -------------------------------------------------------------------------------- 1 | name: "🐛 Report a bug" 2 | description: "Is something not working as you expect?" 3 | labels: [] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for taking the time to file a bug report. 9 | - type: textarea 10 | id: reproduce-bug 11 | attributes: 12 | label: "What steps are needed to reproduce the bug?" 13 | validations: 14 | required: true 15 | - type: textarea 16 | id: expected-behavior 17 | attributes: 18 | label: "What did you expect to happen?" 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: actual-behavior 23 | attributes: 24 | label: "What actually happened?" 25 | description: "For example, what warnings or errors did you get?" 26 | validations: 27 | required: true 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/REPORT_A_DOCS_ISSUE.yml: -------------------------------------------------------------------------------- 1 | name: "📝 Report a docs issue" 2 | description: "Is something wrong, confusing or missing in the docs?" 3 | labels: [] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for taking the time to file a docs issue report. 9 | - type: textarea 10 | id: describe-issue 11 | attributes: 12 | label: "Describe the documentation issue" 13 | validations: 14 | required: true 15 | - type: textarea 16 | id: what-solution 17 | attributes: 18 | label: "What solution would you like to see?" 19 | validations: 20 | required: true 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/REQUEST_A_FEATURE.yml: -------------------------------------------------------------------------------- 1 | name: "🚀 Request a feature" 2 | description: "Do you want to suggest a new feature?" 3 | labels: [] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you for taking the time to request a feature. 9 | - type: textarea 10 | id: what-problem 11 | attributes: 12 | label: "What is the problem you're trying to solve?" 13 | validations: 14 | required: true 15 | - type: textarea 16 | id: what-solution 17 | attributes: 18 | label: "What solution would you like to see?" 19 | validations: 20 | required: true 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | > Which issue, if any, is this issue related to? 4 | 5 | Closes #000 6 | 7 | > Is there anything in the PR that needs further explanation? 8 | 9 | No, it's self explanatory. 10 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: node-ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - "**" 10 | 11 | jobs: 12 | check: 13 | name: Check 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Use Node.js ${{ matrix.node }} 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: "lts/*" 24 | cache: npm 25 | 26 | - name: Install latest npm 27 | run: npm install --global npm@latest 28 | 29 | - name: Install dependencies 30 | run: npm ci 31 | 32 | - name: Check 33 | run: npm run check 34 | 35 | test: 36 | name: Test on Node.js ${{ matrix.node }} and ${{ matrix.os }} 37 | 38 | runs-on: ${{ matrix.os }} 39 | 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | node: [18, 20, 22] 44 | os: [ubuntu-latest, windows-latest] 45 | 46 | steps: 47 | - uses: actions/checkout@v4 48 | 49 | - name: Use Node.js ${{ matrix.node }} 50 | uses: actions/setup-node@v4 51 | with: 52 | node-version: ${{ matrix.node }} 53 | cache: npm 54 | 55 | - name: Install latest npm 56 | run: npm install --global npm@latest 57 | 58 | - name: Install dependencies 59 | run: npm ci 60 | 61 | - name: Test 62 | run: npm test 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 5.0.0 4 | 5 | ## Removed 6 | 7 | - support for Stylelint less than 16.8.2 to use `fix()` callback for autofix ([@planetmalone](https://github.com/planetmalone)) 8 | 9 | ## Fixed 10 | 11 | - npm published files to exclude READMEs and tests 12 | 13 | ## 4.0.0 14 | 15 | ## Removed 16 | 17 | - support for Stylelint less than 16.0.0 to migrate to ESM 18 | 19 | ## 3.1.1 20 | 21 | ### Fixed 22 | 23 | - `alpha-values` false positives for `` 24 | 25 | ## 3.1.0 26 | 27 | ### Added 28 | 29 | - `ignoreFunctionArguments: []` option to `space` rule 30 | 31 | ## 3.0.0 32 | 33 | ### Migrating from 2.0.3 34 | 35 | The package name as been unscoped to `stylelint-scales` and the scoped package has been deprecated. 36 | 37 | You should replace the deprecated package: 38 | 39 | ``` 40 | npm uninstall @signal-noise/stylelint-scales && npm i -D stylelint-scales 41 | ``` 42 | 43 | ```diff json 44 | { 45 | - "plugins": ["@signal-noise/stylelint-scales"], 46 | + "plugins": ["stylelint-scales"], 47 | "rules": { 48 | "scales/alpha-values": [80, 90] 49 | .. 50 | } 51 | } 52 | ``` 53 | 54 | The `font-families` rule has been removed so that the pack is for autofixable numeric scales. You should remove the rule from your config: 55 | 56 | ```diff json 57 | { 58 | "rules": { 59 | - "scales/font-families": ["sans-serif", "serif"] 60 | .. 61 | } 62 | } 63 | ``` 64 | 65 | ### Removed 66 | 67 | - `font-families` rule 68 | 69 | ### Changed 70 | 71 | - Unscoped the package name to `stylelint-scales` 72 | 73 | ### Added 74 | 75 | - `ignoreFunctionArguments: []` option to `font-sizes` rule 76 | 77 | ## 2.0.3 78 | 79 | ### Fixed 80 | 81 | - `stylelint@14` compatibility 82 | 83 | ## 2.0.2 84 | 85 | ### Fixed 86 | 87 | - type error for system keywords in `font-families` 88 | 89 | ## 2.0.1 90 | 91 | ### Fixed 92 | 93 | - parse error for custom properties in `font-families` 94 | 95 | ## 2.0.0 96 | 97 | ### Migrating from 1.5.0 98 | 99 | The plugin pack can now automatically fix all numeric scales! 100 | 101 | A number of breaking changes were needed to make this possible. 102 | 103 | #### Rule names 104 | 105 | A handful of rules were renamed to consistently use plurals: 106 | 107 | - `border-width` to `border-widths` 108 | - `font-family` to `font-families` 109 | - `font-size` to `font-sizes` 110 | - `font-weight` to `font-weights` 111 | - `letter-spacing` to `letter-spacings` 112 | - `line-height` to `line-heights` 113 | - `word-spacing` to `word-spacings` 114 | 115 | For example, you should change the following: 116 | 117 | ```json 118 | { 119 | "rules": { 120 | "scales/font-weight": [400, 600] 121 | } 122 | } 123 | ``` 124 | 125 | To: 126 | 127 | ```json 128 | { 129 | "rules": { 130 | "scales/font-weights": [400, 600] 131 | } 132 | } 133 | ``` 134 | 135 | #### Option signatures 136 | 137 | Rules that check values with units now expect an array of objects for their primary option. Each object must specify two arrays: 138 | 139 | - `scale` - a numerical scale of allowed values 140 | - `units` - a list of units to apply the scale to 141 | 142 | This replaces the `unit` secondary option found on many of the rules. 143 | 144 | For example, you should change the following: 145 | 146 | ```json 147 | { 148 | "rules": { 149 | "scales/font-size": [[1, 2], { "unit": "rem" }] 150 | } 151 | } 152 | ``` 153 | 154 | To: 155 | 156 | ```json 157 | { 158 | "rules": { 159 | "scales/font-sizes": [ 160 | { 161 | "scale": [1, 2], 162 | "units": ["rem"] 163 | } 164 | ] 165 | } 166 | } 167 | ``` 168 | 169 | This will allow: 170 | 171 | ```css 172 | a { 173 | font-size: 1rem; 174 | } 175 | ``` 176 | 177 | You can now specify multiple units per scale, for example: 178 | 179 | ```json 180 | { 181 | "rules": { 182 | "scales/font-sizes": [ 183 | { 184 | "scale": [1, 2], 185 | "units": ["em", "rem"] 186 | } 187 | ] 188 | } 189 | } 190 | ``` 191 | 192 | This will allow: 193 | 194 | ```css 195 | a { 196 | font-size: 1em; 197 | } 198 | 199 | a { 200 | font-size: 1rem; 201 | } 202 | ``` 203 | 204 | And multiple scales per rule, for example: 205 | 206 | ```json 207 | { 208 | "rules": { 209 | "scales/font-sizes": [ 210 | { 211 | "scale": [1, 2], 212 | "units": ["rem"] 213 | }, 214 | { 215 | "scale": [12, 14], 216 | "units": ["px"] 217 | } 218 | ] 219 | } 220 | } 221 | ``` 222 | 223 | This will allow: 224 | 225 | ```css 226 | a { 227 | font-size: 1rem; 228 | } 229 | 230 | a { 231 | font-size: 12px; 232 | } 233 | ``` 234 | 235 | #### Enforcing specific units 236 | 237 | The plugin pack no longer enforces the specified units. This is particularly useful when working with percentages and viewport units, which may not sit on a scale. You should use the [declaration-property-unit-allowed-list rule](https://stylelint.io/user-guide/rules/declaration-property-unit-allowed-list) in stylelint if you wish to enforce specific units. 238 | 239 | For example, you should change the following: 240 | 241 | ```json 242 | { 243 | "rules": { 244 | "scales/font-size": [[1, 2], { "unit": "rem" }] 245 | } 246 | } 247 | ``` 248 | 249 | To: 250 | 251 | ```json 252 | { 253 | "rules": { 254 | "declaration-property-unit-allowed-list": { 255 | "/^font$|^font-size$/": ["rem"] 256 | }, 257 | "scales/font-sizes": [ 258 | { 259 | "scale": [1, 2], 260 | "units": ["rem"] 261 | } 262 | ] 263 | } 264 | } 265 | ``` 266 | 267 | Appropriate regular expressions for the [declaration-property-unit-allowed-list rule](https://stylelint.io/user-guide/rules/declaration-property-unit-allowed-list) are documented in each of the rule READMEs. 268 | 269 | #### Only numeric values 270 | 271 | The rules now, with the exception of `font-families`, only accept numeric values. Non-numeric values in your CSS are now ignored. 272 | 273 | For example, you should change the following: 274 | 275 | ```json 276 | { 277 | "rules": { 278 | "scales/font-weight": [400, 600, "bold"] 279 | } 280 | } 281 | ``` 282 | 283 | To: 284 | 285 | ```json 286 | { 287 | "rules": { 288 | "scales/font-weights": [400, 600] 289 | } 290 | } 291 | ``` 292 | 293 | Numeric font weights are appropriate for both non-variable fonts, e.g. 100, 200, 300 and so on, and [variable fonts](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide), which range from 1 to 1000. 294 | 295 | #### Logical and gap properties 296 | 297 | The rules now check [logical properties](https://drafts.csswg.org/css-logical-1/) and [gap properties](https://drafts.csswg.org/css-align-3/#gaps), so more violations may be caught (and automatically fixed). 298 | 299 | #### Top-level arrays 300 | 301 | You no longer need to enclose top-level scale arrays in an array. 302 | 303 | For example, you should change the following: 304 | 305 | ```json 306 | { 307 | "rules": { 308 | "scales/font-weight": [[400, 600]] 309 | } 310 | } 311 | ``` 312 | 313 | To: 314 | 315 | ```json 316 | { 317 | "rules": { 318 | "scales/font-weights": [400, 600] 319 | } 320 | } 321 | ``` 322 | 323 | #### The `color` rule 324 | 325 | The `color` rule was removed. You should use CSS Variables for colours because, unlike numeric values and font family names, hex values and colour function notation are not human-readable. You can enforce a scale for alpha values using the new `alpha-values` rule. 326 | 327 | For example, you should change the following: 328 | 329 | ```json 330 | { 331 | "rules": { 332 | "scales/color": [ 333 | [ 334 | [0, 0, 0], 335 | [255, 255, 255] 336 | ], 337 | { 338 | "alphaScale": [[0.5, 0.75]] 339 | } 340 | ] 341 | } 342 | } 343 | ``` 344 | 345 | To: 346 | 347 | ```json 348 | { 349 | "rules": { 350 | "scales/alpha-values": [50, 75] 351 | } 352 | } 353 | ``` 354 | 355 | And write your CSS using CSS Variables for colour, for example: 356 | 357 | ```css 358 | a { 359 | color: hsl(var(--accent) / 50%); 360 | } 361 | ``` 362 | 363 | ### Removed 364 | 365 | - `color` rule 366 | 367 | ### Changed 368 | 369 | - names to be consistently pluralised 370 | - options signature for rules that check values with units 371 | - rules now check logical properties and shorthand gap 372 | 373 | ### Added 374 | 375 | - `alpha-values` rule 376 | - autofix to rules that check numeric scales 377 | - per unit scales for rules that check values with units 378 | - support for unenclosed array primary options 379 | 380 | ## 1.5.0 381 | 382 | ### Removed 383 | 384 | - support for Node 8 385 | 386 | ## 1.4.0 387 | 388 | ### Added 389 | 390 | - unit display in messages for all relevant rules 391 | 392 | ## 1.3.0 393 | 394 | ### Added 395 | 396 | - support non-numerical font-weights 397 | 398 | ### Fixed 399 | 400 | - false positives for CSS global keywords in font shorthand declarations 401 | - `border-width` false positives for `none` value 402 | 403 | ## 1.2.0 404 | 405 | ### Added 406 | 407 | - `z-indices` rule 408 | - `border-width` rule 409 | - `unit` option on unit dependent scales 410 | 411 | ## 1.1.1 412 | 413 | ### Fixed 414 | 415 | - missing `sizes` documentation link 416 | 417 | ## 1.1.0 418 | 419 | ### Added 420 | 421 | - `sizes` rule 422 | - `font-family` rule 423 | - `letter-spacing` rule 424 | - `radii` rule 425 | - `word-spacing` rule 426 | 427 | ## 1.0.0 428 | 429 | ### Removed 430 | 431 | - `unit` secondary options from `scales/font-size` and `scales/space` 432 | 433 | ### Changed 434 | 435 | - `scales/font-size` and `scales/space` now check all font-relative and absolute length values 436 | - `scales/space` now checks the `margin`, `padding` and `grip-gap` longhand and shorthand properties, and no longer checks the `box-shadow` and `border` ones 437 | 438 | ### Fixed 439 | 440 | - rationale in `README.md` 441 | - plugin name in example in `README.md` 442 | 443 | ## 0.1.1 444 | 445 | ### Fixed 446 | 447 | - documentation links in `package.json` 448 | 449 | ## 0.1.0 450 | 451 | - initial release 452 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Getting started 4 | 5 | 1. Install [Node.js](https://nodejs.org/en/) 6 | 2. Run `npm install` to install dependencies 7 | 8 | ## Commands 9 | 10 | - `npm test -- --watch` to start interactive test prompt 11 | - `npm test` to run tests 12 | - `npm run check` to run checks 13 | - `npm run fix` to fix check violations 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Signal & Noise Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stylelint-scales 2 | 3 | [![NPM version](https://img.shields.io/npm/v/stylelint-scales.svg)](https://www.npmjs.com/package/stylelint-scales) [![Actions Status](https://github.com/jeddy3/stylelint-scales/workflows/node-ci/badge.svg)](https://github.com/jeddy3/stylelint-scales/actions) [![NPM Downloads](https://img.shields.io/npm/dm/@signal-noise/stylelint-scales.svg)](https://npmcharts.com/compare/@signal-noise/stylelint-scales?minimal=true) 4 | 5 | A [Stylelint](https://stylelint.io) plugin pack to enforce numeric scales. 6 | 7 | ## Installation 8 | 9 | ``` 10 | npm install stylelint-scales --save-dev 11 | ``` 12 | 13 | ## Usage 14 | 15 | Add `stylelint-scales` to your Stylelint config `plugins` array, then add the rules you need to the rules list. All rules from `stylelint-scales` need to be namespaced with `scales`. 16 | 17 | Like so: 18 | 19 | ```json 20 | { 21 | "plugins": ["stylelint-scales"], 22 | "rules": { 23 | "scales/alpha-values": [80, 90], 24 | "scales/border-widths": [{ "scale": [1, 2], "units": ["px"] }], 25 | "scales/font-sizes": [ 26 | [ 27 | { "scale": [1, 1.5, 2], "units": ["em", "rem"] }, 28 | { "scale": [12, 14, 16], "units": ["px"] } 29 | ], 30 | { 31 | "ignoreFunctionArguments": { 32 | "clamp": [1] 33 | } 34 | } 35 | ], 36 | "scales/font-weights": [400, 600], 37 | "scales/line-heights": [1, 1.5], 38 | "scales/radii": [{ "scale": [2, 4], "units": ["px"] }], 39 | "scales/space": [{ "scale": [0.5, 1, 2, 4], "units": ["rem"] }] 40 | } 41 | } 42 | ``` 43 | 44 | To enforce this: 45 | 46 | ```css 47 | p { 48 | border: 1px solid hsl(var(--accent) / 90%)); 49 | border-radius: 2px; 50 | font-size: clamp(1rem, 0.23rem + 1.5vw, 12px); 51 | font-weight: 400; 52 | line-height: 1.5; 53 | margin-block: 2rem; 54 | } 55 | ``` 56 | 57 | This plugin can automatically fix all the scales. 58 | 59 | ## List of rules 60 | 61 | - [`alpha-values`](./lib/rules/alpha-values/README.md): Specify a scale for alpha values (Autofixable). 62 | - [`border-widths`](./lib/rules/border-widths/README.md): Specify a scale for border widths (Autofixable). 63 | - [`font-sizes`](./lib/rules/font-sizes/README.md): Specify a scale for font sizes (Autofixable). 64 | - [`font-weights`](./lib/rules/font-weights/README.md): Specify a scale for font weights (Autofixable). 65 | - [`letter-spacings`](./lib/rules/letter-spacings/README.md): Specify a scale for letter spacings (Autofixable). 66 | - [`line-heights`](./lib/rules/line-heights/README.md): Specify a scale for line heights (Autofixable). 67 | - [`radii`](./lib/rules/radii/README.md): Specify a scale for radii (Autofixable). 68 | - [`sizes`](./lib/rules/sizes/README.md): Specify a scale for sizes (Autofixable). 69 | - [`space`](./lib/rules/space/README.md): Specify a scale for space (Autofixable). 70 | - [`word-spacings`](./lib/rules/word-spacings/README.md): Specify a scale for word spacings (Autofixable). 71 | - [`z-indices`](./lib/rules/z-indices/README.md): Specify a scale for z-indices (Autofixable). 72 | 73 | Ref: [Styled System Keys Reference](https://styled-system.com/theme-specification#key-reference) 74 | 75 | You and the designers should define the scales together. You'll want to strike a balance between code consistency and design flexibility. 76 | 77 | ## Why? 78 | 79 | This plugin can help you create: 80 | 81 | - a consistent look and feel for your websites 82 | - efficient collaboration between you and the designers 83 | 84 | When designers review websites in the browser, they generally use relative terms. For example, "The space between that heading and that paragraph feels too tight, can we make it bigger?" 85 | 86 | You can then pick the next value on the scale and be confident that it'll be consistent with the overall design. 87 | 88 | ### Why not use variables? 89 | 90 | While you can achieve something similar with variables, with this plugin you: 91 | 92 | 1. **Avoid the cognitive load of abstracted variable names**. Colours benefit from human-readable names, whereas it's typically easier to work directly with numeric values. If you use a numeric value that isn't on the scale, the plugin will automatically fix it. 93 | 2. **Enforce code and design consistency with one mechanism**. You likely already use Stylelint for code consistency, for example using the [`unit-allowed-list`](https://stylelint.io/user-guide/rules/unit-allowed-list) to enforce consistent units. By using this plugin, you avoid adding a second mechanism (variables) to ensure design consistency for numeric values. 94 | 3. **Can use the same approach across projects**. The plugin is agnostic of the styling technology, whether that's styled-components, SCSS or vanilla CSS. 95 | 4. **Remove the need to translate values from design tools into variables**. You can copy and paste code from design tools like Figma and Sketch without alteration. 96 | 97 | ## License 98 | 99 | [The MIT License](LICENSE). 100 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import js from "@eslint/js"; 3 | 4 | export default [ 5 | js.configs.recommended, 6 | { 7 | languageOptions: { 8 | globals: { 9 | ...globals.node, 10 | testRule: true, 11 | }, 12 | ecmaVersion: "latest", 13 | sourceType: "module", 14 | }, 15 | }, 16 | ]; 17 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import alphaValues from "./rules/alpha-values/index.js"; 2 | import borderWidths from "./rules/border-widths/index.js"; 3 | import fontSizes from "./rules/font-sizes/index.js"; 4 | import fontWeights from "./rules/font-weights/index.js"; 5 | import letterSpacings from "./rules/letter-spacings/index.js"; 6 | import lineHeights from "./rules/line-heights/index.js"; 7 | import radii from "./rules/radii/index.js"; 8 | import sizes from "./rules/sizes/index.js"; 9 | import space from "./rules/space/index.js"; 10 | import wordSpacings from "./rules/word-spacings/index.js"; 11 | import zIndices from "./rules/z-indices/index.js"; 12 | 13 | export default [ 14 | alphaValues, 15 | borderWidths, 16 | fontSizes, 17 | fontWeights, 18 | letterSpacings, 19 | lineHeights, 20 | radii, 21 | sizes, 22 | space, 23 | wordSpacings, 24 | zIndices, 25 | ]; 26 | -------------------------------------------------------------------------------- /lib/rules/alpha-values/README.md: -------------------------------------------------------------------------------- 1 | # alpha-values 2 | 3 | Specify a scale for alpha values. 4 | 5 | ```css 6 | a { 7 | opacity: 10%; 8 | } 9 | /** ↑ 10 | * This alpha value */ 11 | ``` 12 | 13 | This rule can automatically fix all of the problems reported. 14 | 15 | This rule checks `` alpha values. 16 | 17 | This rule checks within color functions using modern syntax. 18 | 19 | Modern color function syntax and `` alpha values can be enforced, respectively, using the [`color-function-notation`](https://stylelint.io/user-guide/rules/color-function-notation) and [`alpha-value-notation`](https://stylelint.io/user-guide/rules/alpha-value-notation) rules in stylelint. 20 | 21 | ## Options 22 | 23 | `array` 24 | 25 | Given: 26 | 27 | ```json 28 | [10, 20] 29 | ``` 30 | 31 | The following patterns are considered violations: 32 | 33 | ```css 34 | a { 35 | opacity: 5%; 36 | } 37 | ``` 38 | 39 | ```css 40 | a { 41 | color: hsl(0deg 0 0 / 25%); 42 | } 43 | ``` 44 | 45 | ```css 46 | a { 47 | color: hsl(var(--accent) / 50%); 48 | } 49 | ``` 50 | 51 | The following patterns are _not_ considered violations: 52 | 53 | ```css 54 | a { 55 | opacity: 10%; 56 | } 57 | ``` 58 | 59 | ```css 60 | a { 61 | color: hsl(0deg 0 0 / 20%); 62 | } 63 | ``` 64 | 65 | ```css 66 | a { 67 | color: hsl(var(--accent) / 20%); 68 | } 69 | ``` 70 | -------------------------------------------------------------------------------- /lib/rules/alpha-values/index.js: -------------------------------------------------------------------------------- 1 | import { parse } from "postcss-values-parser"; 2 | import stylelint from "stylelint"; 3 | 4 | import createRuleMessages from "../../utils/createRuleMessages.js"; 5 | import getClosest from "../../utils/getClosest.js"; 6 | import getValue from "../../utils/getValue.js"; 7 | import hasNumericScale from "../../utils/hasNumericScale.js"; 8 | import isOnNumericScale from "../../utils/isOnNumericScale.js"; 9 | import setValue from "../../utils/setValue.js"; 10 | 11 | const { 12 | createPlugin, 13 | utils: { report, validateOptions }, 14 | } = stylelint; 15 | 16 | const ruleName = "scales/alpha-values"; 17 | const messages = createRuleMessages(ruleName); 18 | const meta = { 19 | url: "https://github.com/jeddy3/stylelint-scales/blob/main/lib/rules/alpha-values/README.md", 20 | fixable: true, 21 | }; 22 | 23 | const alphaValueProperties = new Set(["opacity", "shape-image-threshold"]); 24 | const alphaValueFunctions = new Set(["hsl", "hwb", "lab", "lch", "rgb"]); 25 | 26 | const rule = (primary) => { 27 | return (root, result) => { 28 | if ( 29 | !validateOptions(result, ruleName, { 30 | actual: primary, 31 | possible: hasNumericScale, 32 | }) 33 | ) 34 | return; 35 | 36 | root.walkDecls((decl) => { 37 | const { prop } = decl; 38 | const value = getValue(decl); 39 | 40 | const valueRoot = parse(value, { 41 | ignoreUnknownWords: true, 42 | }); 43 | 44 | if (isPropertyWithAlphaValue(prop)) { 45 | valueRoot.walkNumerics((node) => { 46 | check(node); 47 | }); 48 | } else { 49 | valueRoot.walkFuncs(({ type, name, nodes }) => { 50 | if (!isFunc(type)) return; 51 | 52 | if (!isFuncWithAlphaValue(name)) return; 53 | 54 | const node = findAlphaValue(nodes); 55 | 56 | if (node) check(node); 57 | }); 58 | } 59 | 60 | function check(node) { 61 | const { value, unit } = node; 62 | 63 | if (unit !== "%") return; 64 | 65 | if (isOnNumericScale(primary, value)) return; 66 | 67 | const fix = () => { 68 | node.value = getClosest(primary, value); 69 | setValue(decl, valueRoot.toString()); 70 | }; 71 | 72 | report({ 73 | fix, 74 | message: messages.expected(value, primary.join(", ")), 75 | node: decl, 76 | result, 77 | ruleName, 78 | word: value, 79 | }); 80 | } 81 | }); 82 | }; 83 | }; 84 | 85 | function isPropertyWithAlphaValue(property) { 86 | return alphaValueProperties.has(property.toLowerCase()); 87 | } 88 | 89 | function isFuncWithAlphaValue(func) { 90 | return alphaValueFunctions.has(func.toLowerCase()); 91 | } 92 | 93 | function isFunc(type) { 94 | return type === "func"; 95 | } 96 | 97 | function findAlphaValue(nodes) { 98 | const slashNodeIndex = nodes.findIndex( 99 | ({ type, value }) => type === "operator" && value === "/", 100 | ); 101 | 102 | if (slashNodeIndex !== -1) { 103 | const nodesAfterSlash = nodes.slice(slashNodeIndex + 1, nodes.length); 104 | return nodesAfterSlash.find( 105 | ({ type, unit }) => type === "numeric" && unit === "%", 106 | ); 107 | } 108 | } 109 | 110 | rule.primaryOptionArray = true; 111 | 112 | rule.ruleName = ruleName; 113 | rule.messages = messages; 114 | rule.meta = meta; 115 | 116 | export default createPlugin(ruleName, rule); 117 | -------------------------------------------------------------------------------- /lib/rules/alpha-values/index.test.js: -------------------------------------------------------------------------------- 1 | import { stripIndent } from "common-tags"; 2 | import { testRule } from "stylelint-test-rule-node"; 3 | 4 | import plugin from "./index.js"; 5 | const { 6 | rule: { messages, ruleName }, 7 | } = plugin; 8 | 9 | testRule({ 10 | ruleName, 11 | config: [10, 20], 12 | fix: true, 13 | plugins: [plugin], 14 | 15 | accept: [ 16 | { 17 | code: "a { opacity: 10%; }", 18 | description: "Value on scale", 19 | }, 20 | { 21 | code: "a { opacity: 0.5; }", 22 | description: "Number value", 23 | }, 24 | { 25 | code: "a { color: hsl(0deg 0% 0% / 10%) }", 26 | description: "Value on scale in function", 27 | }, 28 | { 29 | code: "a { color: hsl(0deg 0% 0% / 0.5) }", 30 | description: "Number value in function", 31 | }, 32 | { 33 | code: "a { color: hsl(0deg 0% 0%) }", 34 | description: "Value on scale in function", 35 | }, 36 | { 37 | code: "a { color: hsl(var(--accent) / 10%) }", 38 | description: "Value on scale in function with var", 39 | }, 40 | ], 41 | 42 | reject: [ 43 | { 44 | code: "a { opacity: 5% }", 45 | fixed: "a { opacity: 10% }", 46 | message: messages.expected("5", "10, 20"), 47 | line: 1, 48 | column: 14, 49 | description: "Value off scale", 50 | }, 51 | { 52 | code: "a { shape-image-threshold: 25% }", 53 | fixed: "a { shape-image-threshold: 20% }", 54 | message: messages.expected("25", "10, 20"), 55 | line: 1, 56 | column: 28, 57 | description: "Value off scale for shape", 58 | }, 59 | { 60 | code: "a { color: hsl(0deg 0% 0% / 5%) }", 61 | fixed: "a { color: hsl(0deg 0% 0% / 10%) }", 62 | message: messages.expected("5", "10, 20"), 63 | line: 1, 64 | column: 29, 65 | description: "Value off scale in function", 66 | }, 67 | { 68 | code: "a { color: hsl(0deg 0% 0% / /* comment */ 5% /* comment */) }", 69 | fixed: "a { color: hsl(0deg 0% 0% / /* comment */ 10% /* comment */) }", 70 | message: messages.expected("5", "10, 20"), 71 | line: 1, 72 | column: 43, 73 | description: "Value off scale in function with comments", 74 | }, 75 | { 76 | code: "a { color: rgb(0 0 0 / 5%) }", 77 | fixed: "a { color: rgb(0 0 0 / 10%) }", 78 | message: messages.expected("5", "10, 20"), 79 | line: 1, 80 | column: 24, 81 | description: "Value off scale in rgb function", 82 | }, 83 | { 84 | code: "a { color: hsl(var(--accent) / 5%) }", 85 | fixed: "a { color: hsl(var(--accent) / 10%) }", 86 | message: messages.expected("5", "10, 20"), 87 | line: 1, 88 | column: 32, 89 | description: "Value off scale in function with var", 90 | }, 91 | { 92 | code: stripIndent` 93 | a { 94 | background-image: linear-gradient( 95 | to right, 96 | hwb(0deg 0% 0% / 5%) 97 | lch(0% 0 0 / 25%) 98 | ); 99 | } 100 | `, 101 | fixed: stripIndent` 102 | a { 103 | background-image: linear-gradient( 104 | to right, 105 | hwb(0deg 0% 0% / 10%) 106 | lch(0% 0 0 / 20%) 107 | ); 108 | } 109 | `, 110 | warnings: [ 111 | { message: messages.expected("5", "10, 20"), line: 4, column: 22 }, 112 | { message: messages.expected("25", "10, 20"), line: 5, column: 18 }, 113 | ], 114 | description: "Values off scale", 115 | }, 116 | ], 117 | }); 118 | -------------------------------------------------------------------------------- /lib/rules/border-widths/README.md: -------------------------------------------------------------------------------- 1 | # border-widths 2 | 3 | Specify scales for border widths. 4 | 5 | ```css 6 | a { 7 | border-width: 0.1rem; 8 | } 9 | /** ↑ 10 | * This width */ 11 | ``` 12 | 13 | This rule can automatically fix all of the problems reported. 14 | 15 | This rule checks `` values. 16 | 17 | This rule can be paired with the [`declaration-property-unit-allowed-list`](https://stylelint.io/user-guide/rules/declaration-property-unit-allowed-list) rule in stylelint, using the RegEx: 18 | 19 | ``` 20 | /^border$|^border.*(width$|top$|right$|bottom$|left$/ 21 | ``` 22 | 23 | ## Options 24 | 25 | `array` of `objects` as `{scale: [], units: []}` 26 | 27 | Given: 28 | 29 | ```json 30 | [{ "scale": [1, 2], "units": ["px"] }] 31 | ``` 32 | 33 | The following patterns are considered violations: 34 | 35 | ```css 36 | a { 37 | border-width: 3px; 38 | } 39 | ``` 40 | 41 | ```css 42 | a { 43 | border: 3px solid red; 44 | } 45 | ``` 46 | 47 | The following patterns are _not_ considered violations: 48 | 49 | ```css 50 | a { 51 | border-width: 1px; 52 | } 53 | ``` 54 | 55 | ```css 56 | a { 57 | border: 2px solid red; 58 | } 59 | ``` 60 | -------------------------------------------------------------------------------- /lib/rules/border-widths/index.js: -------------------------------------------------------------------------------- 1 | import { parse } from "postcss-values-parser"; 2 | import stylelint from "stylelint"; 3 | 4 | import createRuleMessages from "../../utils/createRuleMessages.js"; 5 | import findScaleByUnit from "../../utils/findScaleByUnit.js"; 6 | import getClosest from "../../utils/getClosest.js"; 7 | import getValue from "../../utils/getValue.js"; 8 | import hasScalesWithUnits from "../../utils/hasScalesWithUnits.js"; 9 | import isOnNumericScale from "../../utils/isOnNumericScale.js"; 10 | import setValue from "../../utils/setValue.js"; 11 | 12 | const { 13 | createPlugin, 14 | utils: { report, validateOptions }, 15 | } = stylelint; 16 | 17 | const ruleName = "scales/border-widths"; 18 | const messages = createRuleMessages(ruleName); 19 | const meta = { 20 | url: "https://github.com/jeddy3/stylelint-scales/blob/main/lib/rules/border-widths/README.md", 21 | fixable: true, 22 | }; 23 | 24 | const propertyFilter = 25 | /^border$|^border.*(width$|top$|right$|bottom$|left$|block$|inline$|start$|end$)/; 26 | 27 | const rule = (primary) => { 28 | return (root, result) => { 29 | if ( 30 | !validateOptions(result, ruleName, { 31 | actual: primary, 32 | possible: hasScalesWithUnits, 33 | }) 34 | ) 35 | return; 36 | 37 | root.walkDecls(propertyFilter, (decl) => { 38 | const value = getValue(decl); 39 | 40 | const valueRoot = parse(value, { 41 | ignoreUnknownWords: true, 42 | }); 43 | 44 | valueRoot.walkNumerics((node) => { 45 | check(node); 46 | }); 47 | 48 | function check(node) { 49 | const { value, unit } = node; 50 | 51 | const scale = findScaleByUnit(primary, unit); 52 | 53 | if (isOnNumericScale(scale, value)) return; 54 | 55 | const fix = () => { 56 | node.value = getClosest(scale, value); 57 | setValue(decl, valueRoot.toString()); 58 | }; 59 | 60 | report({ 61 | fix, 62 | message: messages.expected(value, scale.join(", "), unit), 63 | node: decl, 64 | result, 65 | ruleName, 66 | word: value, 67 | }); 68 | } 69 | }); 70 | }; 71 | }; 72 | 73 | rule.primaryOptionArray = true; 74 | 75 | rule.ruleName = ruleName; 76 | rule.messages = messages; 77 | rule.meta = meta; 78 | 79 | export default createPlugin(ruleName, rule); 80 | -------------------------------------------------------------------------------- /lib/rules/border-widths/index.test.js: -------------------------------------------------------------------------------- 1 | import { testRule } from "stylelint-test-rule-node"; 2 | 3 | import plugin from "./index.js"; 4 | 5 | const { 6 | rule: { messages, ruleName }, 7 | } = plugin; 8 | 9 | testRule({ 10 | ruleName, 11 | config: [ 12 | { 13 | scale: [1, 2], 14 | units: ["px"], 15 | }, 16 | ], 17 | fix: true, 18 | plugins: [plugin], 19 | 20 | accept: [ 21 | { 22 | code: "a { border-width: 1px; }", 23 | description: "Value on scale", 24 | }, 25 | { 26 | code: "a { border: 1px solid grey; }", 27 | description: "Value on scale in shorthand", 28 | }, 29 | { 30 | code: "a { width: 3px; }", 31 | description: "Ignored property", 32 | }, 33 | { 34 | code: "a { border-width: 3vh; }", 35 | description: "Ignored unit", 36 | }, 37 | { 38 | code: "a { border: none; }", 39 | description: "Ignored keyword", 40 | }, 41 | ], 42 | 43 | reject: [ 44 | { 45 | code: "a { border-width: 3px }", 46 | fixed: "a { border-width: 2px }", 47 | message: messages.expected("3", "1, 2", "px"), 48 | line: 1, 49 | column: 19, 50 | description: "Value off scale", 51 | }, 52 | { 53 | code: "a { border-block-width: 3px }", 54 | fixed: "a { border-block-width: 2px }", 55 | message: messages.expected("3", "1, 2", "px"), 56 | line: 1, 57 | column: 25, 58 | description: "Value off scale in logical property", 59 | }, 60 | { 61 | code: "a { border-width: 3px 1.5px }", 62 | fixed: "a { border-width: 2px 1px }", 63 | warnings: [ 64 | { 65 | message: messages.expected("3", "1, 2", "px"), 66 | line: 1, 67 | column: 19, 68 | }, 69 | { 70 | message: messages.expected("1.5", "1, 2", "px"), 71 | line: 1, 72 | column: 23, 73 | }, 74 | ], 75 | description: "Multiple values off scale", 76 | }, 77 | { 78 | code: "a { border: 3px solid grey; }", 79 | fixed: "a { border: 2px solid grey; }", 80 | message: messages.expected("3", "1, 2", "px"), 81 | line: 1, 82 | column: 13, 83 | description: "Value off scale in shorthand", 84 | }, 85 | { 86 | code: "a { border-top: 3px solid grey; }", 87 | fixed: "a { border-top: 2px solid grey; }", 88 | message: messages.expected("3", "1, 2", "px"), 89 | line: 1, 90 | column: 17, 91 | description: "Value off scale in directional shorthand", 92 | }, 93 | { 94 | code: "a { border-block-end: 3px solid grey; }", 95 | fixed: "a { border-block-end: 2px solid grey; }", 96 | message: messages.expected("3", "1, 2", "px"), 97 | line: 1, 98 | column: 23, 99 | description: "Value off scale in directional logical shorthand", 100 | }, 101 | { 102 | code: "a { border: 3px solid hsl(10deg 10% 5%); }", 103 | fixed: "a { border: 2px solid hsl(10deg 10% 5%); }", 104 | message: messages.expected("3", "1, 2", "px"), 105 | line: 1, 106 | column: 13, 107 | description: "Value off scale in shorthand with color function", 108 | }, 109 | ], 110 | }); 111 | -------------------------------------------------------------------------------- /lib/rules/font-sizes/README.md: -------------------------------------------------------------------------------- 1 | # font-sizes 2 | 3 | Specify scales for font sizes. 4 | 5 | ```css 6 | a { 7 | font-size: 1rem; 8 | } 9 | /** ↑ 10 | * This size */ 11 | ``` 12 | 13 | This rule can automatically fix all of the problems reported. 14 | 15 | This rule checks `` and `` values. 16 | 17 | This rule can be paired with the [`declaration-property-unit-allowed-list`](https://stylelint.io/user-guide/rules/declaration-property-unit-allowed-list) rule in stylelint, using the RegEx: 18 | 19 | ``` 20 | /^font-size$|^font$/ 21 | ``` 22 | 23 | ## Options 24 | 25 | `array` of `objects` as `{scale: [], units: []}` 26 | 27 | Given: 28 | 29 | ```json 30 | [ 31 | { "scale": [1, 2], "units": ["em", "rem"] }, 32 | { "scale": [16, 32], "units": ["px"] } 33 | ] 34 | ``` 35 | 36 | The following patterns are considered violations: 37 | 38 | ```css 39 | a { 40 | font-size: 16rem; 41 | } 42 | ``` 43 | 44 | ```css 45 | a { 46 | font: 2px/1 serif; 47 | } 48 | ``` 49 | 50 | The following patterns are _not_ considered violations: 51 | 52 | ```css 53 | a { 54 | font-size: 1rem; 55 | } 56 | ``` 57 | 58 | ```css 59 | a { 60 | font: 16px/1 serif; 61 | } 62 | ``` 63 | 64 | ## Optional secondary options 65 | 66 | ### `ignoreFunctionArguments: { "function-name": [] }` 67 | 68 | Given: 69 | 70 | ```json 71 | [ 72 | [{ "scale": [1, 2], "units": ["rem"] }], 73 | { 74 | "ignoreFunctionArguments": { "clamp": [1], "min": [0, 1] } 75 | } 76 | ] 77 | ``` 78 | 79 | The following patterns are _not_ considered problems: 80 | 81 | ```css 82 | a { 83 | font-size: clamp(1rem, 0.37rem + 0.45vw, 2rem); 84 | } 85 | ``` 86 | 87 | ```css 88 | a { 89 | font-size: min(3rem, 4rem); 90 | } 91 | ``` 92 | -------------------------------------------------------------------------------- /lib/rules/font-sizes/index.js: -------------------------------------------------------------------------------- 1 | import { parse } from "postcss-values-parser"; 2 | import stylelint from "stylelint"; 3 | 4 | import createRuleMessages from "../../utils/createRuleMessages.js"; 5 | import findScaleByUnit from "../../utils/findScaleByUnit.js"; 6 | import getClosest from "../../utils/getClosest.js"; 7 | import getValue from "../../utils/getValue.js"; 8 | import hasScalesWithUnits from "../../utils/hasScalesWithUnits.js"; 9 | import hasObjectWithNumericArray from "../../utils/hasObjectWithNumericArray.js"; 10 | import isIgnoredFunctionArgument from "../../utils/isIgnoredFunctionArgument.js"; 11 | import isLineHeight from "../../utils/isLineHeight.js"; 12 | import isOnNumericScale from "../../utils/isOnNumericScale.js"; 13 | import setValue from "../../utils/setValue.js"; 14 | 15 | const { 16 | createPlugin, 17 | utils: { report, validateOptions }, 18 | } = stylelint; 19 | 20 | const ruleName = "scales/font-sizes"; 21 | const messages = createRuleMessages(ruleName); 22 | const meta = { 23 | url: "https://github.com/jeddy3/stylelint-scales/blob/main/lib/rules/font-sizes/README.md", 24 | fixable: true, 25 | }; 26 | 27 | const propertyFilter = /^font-size$|^font$/; 28 | 29 | const rule = (primary, secondary) => { 30 | return (root, result) => { 31 | if ( 32 | !validateOptions( 33 | result, 34 | ruleName, 35 | { 36 | actual: primary, 37 | possible: hasScalesWithUnits, 38 | }, 39 | { 40 | optional: true, 41 | actual: secondary, 42 | possible: { 43 | ignoreFunctionArguments: hasObjectWithNumericArray, 44 | }, 45 | }, 46 | ) 47 | ) 48 | return; 49 | 50 | const ignoreFunctionArguments = secondary?.ignoreFunctionArguments; 51 | 52 | root.walkDecls(propertyFilter, (decl) => { 53 | const { prop } = decl; 54 | const value = getValue(decl); 55 | 56 | const valueRoot = parse(value, { 57 | ignoreUnknownWords: true, 58 | }); 59 | 60 | switch (prop) { 61 | case "font": { 62 | const node = findFontSize(valueRoot.nodes); 63 | if (node) check(node); 64 | break; 65 | } 66 | case "font-size": 67 | valueRoot.walkNumerics((node) => { 68 | check(node); 69 | }); 70 | break; 71 | } 72 | 73 | function check(node) { 74 | const { value, unit } = node; 75 | 76 | if (isIgnoredFunctionArgument(node, ignoreFunctionArguments)) return; 77 | 78 | const scale = findScaleByUnit(primary, unit); 79 | 80 | if (isOnNumericScale(scale, value)) return; 81 | 82 | const fix = () => { 83 | node.value = getClosest(scale, value); 84 | setValue(decl, valueRoot.toString()); 85 | }; 86 | 87 | report({ 88 | fix, 89 | message: messages.expected(value, scale.join(", "), unit), 90 | node: decl, 91 | result, 92 | ruleName, 93 | word: value, 94 | }); 95 | } 96 | }); 97 | }; 98 | }; 99 | 100 | function findFontSize(nodes) { 101 | const node = nodes.find( 102 | ({ type, unit }) => type === "numeric" && unit !== "", 103 | ); 104 | 105 | if (node && !isLineHeight(node)) return node; 106 | } 107 | 108 | rule.primaryOptionArray = true; 109 | 110 | rule.ruleName = ruleName; 111 | rule.messages = messages; 112 | rule.meta = meta; 113 | 114 | export default createPlugin(ruleName, rule); 115 | -------------------------------------------------------------------------------- /lib/rules/font-sizes/index.test.js: -------------------------------------------------------------------------------- 1 | import { testRule } from "stylelint-test-rule-node"; 2 | 3 | import plugin from "./index.js"; 4 | 5 | const { 6 | rule: { messages, ruleName }, 7 | } = plugin; 8 | 9 | testRule({ 10 | ruleName, 11 | config: [ 12 | { 13 | scale: [1, 2], 14 | units: ["em", "rem"], 15 | }, 16 | ], 17 | fix: true, 18 | plugins: [plugin], 19 | 20 | accept: [ 21 | { 22 | code: "a { font-size: 1rem; }", 23 | description: "Value on scale", 24 | }, 25 | { 26 | code: "a { font: 2em serif; }", 27 | description: "Value on scale in shorthand", 28 | }, 29 | { 30 | code: "a { font: 400 2em/3rem serif; }", 31 | description: "Value on scale in shorthand with weight and line height", 32 | }, 33 | ], 34 | 35 | reject: [ 36 | { 37 | code: "a { font-size: 3em; }", 38 | fixed: "a { font-size: 2em; }", 39 | message: messages.expected("3", "1, 2", "em"), 40 | line: 1, 41 | column: 16, 42 | description: "Value off scale", 43 | }, 44 | { 45 | code: "a { font: 3rem sans-serif; }", 46 | fixed: "a { font: 2rem sans-serif; }", 47 | message: messages.expected("3", "1, 2", "rem"), 48 | line: 1, 49 | column: 11, 50 | description: "Value off scale in shorthand", 51 | }, 52 | { 53 | code: "a { font: 700 3em/4em sans-serif; }", 54 | fixed: "a { font: 700 2em/4em sans-serif; }", 55 | message: messages.expected("3", "1, 2", "em"), 56 | line: 1, 57 | column: 15, 58 | description: 59 | "Value off scale in shorthand with font weight and line-height", 60 | }, 61 | ], 62 | }); 63 | 64 | testRule({ 65 | ruleName, 66 | config: [ 67 | [ 68 | { 69 | scale: [1, 2], 70 | units: ["rem"], 71 | }, 72 | ], 73 | { 74 | ignoreFunctionArguments: { 75 | clamp: [1], 76 | min: [0], 77 | max: [0, 1], 78 | }, 79 | }, 80 | ], 81 | fix: true, 82 | plugins: [plugin], 83 | 84 | accept: [ 85 | { 86 | code: "a { font-size: 1rem; }", 87 | description: "Value on scale", 88 | }, 89 | { 90 | code: "a { font-size: clamp(1rem, 0.5rem + 0.5vw, 2rem); }", 91 | description: "Value off scale for ignored argument", 92 | }, 93 | { 94 | code: "a { font-size: min(0.5rem, 1rem); }", 95 | description: "Value off scale for ignored argument", 96 | }, 97 | { 98 | code: "a { font-size: max(0.5rem, 2.5rem); }", 99 | description: "Two values off scale both ignored", 100 | }, 101 | ], 102 | 103 | reject: [ 104 | { 105 | code: "a { font-size: 3rem; }", 106 | fixed: "a { font-size: 2rem; }", 107 | message: messages.expected("3", "1, 2", "rem"), 108 | line: 1, 109 | column: 16, 110 | description: "Value off scale", 111 | }, 112 | { 113 | code: "a { font-size: min(0.5rem, 2.5rem); }", 114 | fixed: "a { font-size: min(0.5rem, 2rem); }", 115 | message: messages.expected("2.5", "1, 2", "rem"), 116 | line: 1, 117 | column: 28, 118 | description: "Two values off scale with one ignored", 119 | }, 120 | { 121 | code: "a { font-size: clamp(0.5rem, 0.3rem + 0.5vw, 2.5rem); }", 122 | fixed: "a { font-size: clamp(1rem, 0.3rem + 0.5vw, 2rem); }", 123 | warnings: [ 124 | { 125 | message: messages.expected("0.5", "1, 2", "rem"), 126 | line: 1, 127 | column: 22, 128 | }, 129 | { 130 | message: messages.expected("2.5", "1, 2", "rem"), 131 | line: 1, 132 | column: 46, 133 | }, 134 | ], 135 | description: "Two values off scale", 136 | }, 137 | ], 138 | }); 139 | -------------------------------------------------------------------------------- /lib/rules/font-weights/README.md: -------------------------------------------------------------------------------- 1 | # font-weights 2 | 3 | Specify a scale for font weights. 4 | 5 | ```css 6 | a { 7 | font-weight: 400; 8 | } 9 | /** ↑ 10 | * This weight */ 11 | ``` 12 | 13 | This rule can automatically fix all of the problems reported. 14 | 15 | This rule checks numeric font weights. 16 | 17 | Numeric font-weights can be enforced using the [`font-weight-notation`](https://stylelint.io/user-guide/rules/font-weight-notation) rule in stylelint. 18 | 19 | ## Options 20 | 21 | `array` 22 | 23 | Given: 24 | 25 | ```json 26 | [400, 700] 27 | ``` 28 | 29 | The following patterns are considered violations: 30 | 31 | ```css 32 | a { 33 | font-weight: 300; 34 | } 35 | ``` 36 | 37 | ```css 38 | a { 39 | font: 900 1rem/1 serif; 40 | } 41 | ``` 42 | 43 | The following patterns are _not_ considered violations: 44 | 45 | ```css 46 | a { 47 | font-weight: 400; 48 | } 49 | ``` 50 | 51 | ```css 52 | a { 53 | font: 700 1rem/1 serif; 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /lib/rules/font-weights/index.js: -------------------------------------------------------------------------------- 1 | import { parse } from "postcss-values-parser"; 2 | import stylelint from "stylelint"; 3 | 4 | import createRuleMessages from "../../utils/createRuleMessages.js"; 5 | import getClosest from "../../utils/getClosest.js"; 6 | import getValue from "../../utils/getValue.js"; 7 | import hasNumericScale from "../../utils/hasNumericScale.js"; 8 | import isLineHeight from "../../utils/isLineHeight.js"; 9 | import isOnNumericScale from "../../utils/isOnNumericScale.js"; 10 | import setValue from "../../utils/setValue.js"; 11 | 12 | const { 13 | createPlugin, 14 | utils: { report, validateOptions }, 15 | } = stylelint; 16 | 17 | const ruleName = "scales/font-weights"; 18 | const messages = createRuleMessages(ruleName); 19 | const meta = { 20 | url: "https://github.com/jeddy3/stylelint-scales/blob/main/lib/rules/font-weights/README.md", 21 | fixable: true, 22 | }; 23 | 24 | const propertyFilter = /^font-weight$|^font$/; 25 | 26 | const rule = (primary) => { 27 | return (root, result) => { 28 | if ( 29 | !validateOptions(result, ruleName, { 30 | actual: primary, 31 | possible: hasNumericScale, 32 | }) 33 | ) 34 | return; 35 | 36 | root.walkDecls(propertyFilter, (decl) => { 37 | const { prop } = decl; 38 | const value = getValue(decl); 39 | 40 | const valueRoot = parse(value, { 41 | ignoreUnknownWords: true, 42 | }); 43 | 44 | switch (prop) { 45 | case "font": { 46 | const node = findFontWeight(valueRoot.nodes); 47 | if (node) check(node); 48 | break; 49 | } 50 | case "font-weight": 51 | valueRoot.walkNumerics((node) => { 52 | check(node); 53 | }); 54 | break; 55 | } 56 | 57 | function check(node) { 58 | const { value, unit } = node; 59 | 60 | if (unit) return; 61 | 62 | if (isOnNumericScale(primary, value)) return; 63 | 64 | const fix = () => { 65 | node.value = getClosest(primary, value); 66 | setValue(decl, valueRoot.toString()); 67 | }; 68 | 69 | report({ 70 | fix, 71 | message: messages.expected(value, primary.join(", ")), 72 | node: decl, 73 | result, 74 | ruleName, 75 | word: value, 76 | }); 77 | } 78 | }); 79 | }; 80 | }; 81 | 82 | function findFontWeight(nodes) { 83 | const node = nodes.find( 84 | ({ type, unit }) => type === "numeric" && unit === "", 85 | ); 86 | 87 | if (node && !isLineHeight(node)) return node; 88 | } 89 | 90 | rule.primaryOptionArray = true; 91 | 92 | rule.ruleName = ruleName; 93 | rule.messages = messages; 94 | rule.meta = meta; 95 | 96 | export default createPlugin(ruleName, rule); 97 | -------------------------------------------------------------------------------- /lib/rules/font-weights/index.test.js: -------------------------------------------------------------------------------- 1 | import { testRule } from "stylelint-test-rule-node"; 2 | 3 | import plugin from "./index.js"; 4 | 5 | const { 6 | rule: { messages, ruleName }, 7 | } = plugin; 8 | 9 | testRule({ 10 | ruleName, 11 | config: [400, 700], 12 | fix: true, 13 | plugins: [plugin], 14 | 15 | accept: [ 16 | { 17 | code: "a { font-weight: 400; }", 18 | description: "Value on scale", 19 | }, 20 | { 21 | code: "a { font: 400 1px/2 serif; }", 22 | description: "Value on scale in shorthand", 23 | }, 24 | { 25 | code: "a { font-weight: bold; }", 26 | description: "Ignored value", 27 | }, 28 | { 29 | code: "a { font: bold 1px/2 serif; }", 30 | description: "Ignored value in shorthand", 31 | }, 32 | { 33 | code: "a { font: 1px serif; }", 34 | description: "Ignored font-weightless shorthand", 35 | }, 36 | { 37 | code: "a { font: 1px/2 serif; }", 38 | description: 39 | "Ignored font-weightless shorthand with unitless line height", 40 | }, 41 | ], 42 | 43 | reject: [ 44 | { 45 | code: "a { font-weight: 200 }", 46 | fixed: "a { font-weight: 400 }", 47 | message: messages.expected("200", "400, 700"), 48 | line: 1, 49 | column: 18, 50 | description: "Value off scale", 51 | }, 52 | { 53 | code: "a { font: 600 3px/3 sans-serif; }", 54 | fixed: "a { font: 700 3px/3 sans-serif; }", 55 | message: messages.expected("600", "400, 700"), 56 | line: 1, 57 | column: 11, 58 | description: "Value off scale in shorthand", 59 | }, 60 | { 61 | code: "a { font: small-caps 900 3px sans-serif; }", 62 | fixed: "a { font: small-caps 700 3px sans-serif; }", 63 | message: messages.expected("900", "400, 700"), 64 | line: 1, 65 | column: 22, 66 | description: "Value off scale in shorthand with preceeding font-variant", 67 | }, 68 | ], 69 | }); 70 | -------------------------------------------------------------------------------- /lib/rules/letter-spacings/README.md: -------------------------------------------------------------------------------- 1 | # letter-spacings 2 | 3 | Specify scales for letter spacing. 4 | 5 | ```css 6 | a { 7 | letter-spacing: 0.1rem; 8 | } 9 | /** ↑ 10 | * This letter spacing */ 11 | ``` 12 | 13 | This rule can automatically fix all of the problems reported. 14 | 15 | This rule checks `` values. 16 | 17 | This rule can be paired with the [`declaration-property-unit-allowed-list`](https://stylelint.io/user-guide/rules/declaration-property-unit-allowed-list) rule in stylelint. 18 | 19 | ## Options 20 | 21 | `array` of `objects` as `{scale: [], units: []}` 22 | 23 | Given: 24 | 25 | ```json 26 | { 27 | "scale": [0.1, 0.2], 28 | "units": ["rem"] 29 | } 30 | ``` 31 | 32 | The following patterns are considered violations: 33 | 34 | ```css 35 | a { 36 | letter-spacing: 0.5rem; 37 | } 38 | ``` 39 | 40 | The following patterns are _not_ considered violations: 41 | 42 | ```css 43 | a { 44 | letter-spacing: 0.1rem; 45 | } 46 | 47 | a { 48 | letter-spacing: 0.2rem; 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /lib/rules/letter-spacings/index.js: -------------------------------------------------------------------------------- 1 | import { parse } from "postcss-values-parser"; 2 | import stylelint from "stylelint"; 3 | 4 | import createRuleMessages from "../../utils/createRuleMessages.js"; 5 | import findScaleByUnit from "../../utils/findScaleByUnit.js"; 6 | import getClosest from "../../utils/getClosest.js"; 7 | import getValue from "../../utils/getValue.js"; 8 | import hasScalesWithUnits from "../../utils/hasScalesWithUnits.js"; 9 | import isOnNumericScale from "../../utils/isOnNumericScale.js"; 10 | import setValue from "../../utils/setValue.js"; 11 | 12 | const { 13 | createPlugin, 14 | utils: { report, validateOptions }, 15 | } = stylelint; 16 | 17 | const ruleName = "scales/letter-spacings"; 18 | const messages = createRuleMessages(ruleName); 19 | const meta = { 20 | url: "https://github.com/jeddy3/stylelint-scales/blob/main/lib/rules/letter-spacings/README.md", 21 | fixable: true, 22 | }; 23 | 24 | const propertyFilter = "letter-spacing"; 25 | 26 | const rule = (primary) => { 27 | return (root, result) => { 28 | if ( 29 | !validateOptions(result, ruleName, { 30 | actual: primary, 31 | possible: hasScalesWithUnits, 32 | }) 33 | ) 34 | return; 35 | 36 | root.walkDecls(propertyFilter, (decl) => { 37 | const value = getValue(decl); 38 | 39 | const valueRoot = parse(value, { 40 | ignoreUnknownWords: true, 41 | }); 42 | 43 | valueRoot.walkNumerics((node) => { 44 | check(node); 45 | }); 46 | 47 | function check(node) { 48 | const { value, unit } = node; 49 | 50 | const scale = findScaleByUnit(primary, unit); 51 | 52 | if (isOnNumericScale(scale, value)) return; 53 | 54 | const fix = () => { 55 | node.value = getClosest(scale, value); 56 | setValue(decl, valueRoot.toString()); 57 | }; 58 | 59 | report({ 60 | fix, 61 | message: messages.expected(value, scale.join(", "), unit), 62 | node: decl, 63 | result, 64 | ruleName, 65 | word: value, 66 | }); 67 | } 68 | }); 69 | }; 70 | }; 71 | 72 | rule.primaryOptionArray = true; 73 | 74 | rule.ruleName = ruleName; 75 | rule.messages = messages; 76 | rule.meta = meta; 77 | 78 | export default createPlugin(ruleName, rule); 79 | -------------------------------------------------------------------------------- /lib/rules/letter-spacings/index.test.js: -------------------------------------------------------------------------------- 1 | import { testRule } from "stylelint-test-rule-node"; 2 | 3 | import plugin from "./index.js"; 4 | 5 | const { 6 | rule: { messages, ruleName }, 7 | } = plugin; 8 | 9 | testRule({ 10 | ruleName, 11 | config: [ 12 | { 13 | scale: [-1, 2], 14 | units: ["px"], 15 | }, 16 | ], 17 | fix: true, 18 | plugins: [plugin], 19 | 20 | accept: [ 21 | { 22 | code: "a { letter-spacing: -1px; }", 23 | description: "Value on scale", 24 | }, 25 | { 26 | code: "a { width: 3px; }", 27 | description: "Ignored property", 28 | }, 29 | { 30 | code: "a { letter-spacing: 3vh; }", 31 | description: "Ignored unit", 32 | }, 33 | ], 34 | 35 | reject: [ 36 | { 37 | code: "a { letter-spacing: 3px; }", 38 | fixed: "a { letter-spacing: 2px; }", 39 | message: messages.expected("3", "-1, 2", "px"), 40 | line: 1, 41 | column: 21, 42 | description: "Value off scale", 43 | }, 44 | { 45 | code: "a { letter-spacing: -0.5px; }", 46 | fixed: "a { letter-spacing: -1px; }", 47 | message: messages.expected("-0.5", "-1, 2", "px"), 48 | line: 1, 49 | column: 21, 50 | description: "Negative value off scale", 51 | }, 52 | ], 53 | }); 54 | -------------------------------------------------------------------------------- /lib/rules/line-heights/README.md: -------------------------------------------------------------------------------- 1 | # line-heights 2 | 3 | Specify a scale for line heights. 4 | 5 | ```css 6 | a { 7 | line-height: 1; 8 | } 9 | /** ↑ 10 | * This line-height */ 11 | ``` 12 | 13 | This rule can automatically fix all of the problems reported. 14 | 15 | This rule checks unitless line heights. 16 | 17 | This rule can be paired with the [`declaration-property-unit-allowed-list`](https://stylelint.io/user-guide/rules/declaration-property-unit-allowed-list) rule in stylelint. 18 | 19 | ## Options 20 | 21 | `array` 22 | 23 | Given: 24 | 25 | ```json 26 | [1, 1.5] 27 | ``` 28 | 29 | The following patterns are considered violations: 30 | 31 | ```css 32 | a { 33 | line-height: 2; 34 | } 35 | ``` 36 | 37 | ```css 38 | a { 39 | font: 2rem/3 serif; 40 | } 41 | ``` 42 | 43 | The following patterns are _not_ considered violations: 44 | 45 | ```css 46 | a { 47 | line-height: 1; 48 | } 49 | ``` 50 | 51 | ```css 52 | a { 53 | font: 2rem/1.5 serif; 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /lib/rules/line-heights/index.js: -------------------------------------------------------------------------------- 1 | import { parse } from "postcss-values-parser"; 2 | import stylelint from "stylelint"; 3 | 4 | import createRuleMessages from "../../utils/createRuleMessages.js"; 5 | import getClosest from "../../utils/getClosest.js"; 6 | import getValue from "../../utils/getValue.js"; 7 | import hasNumericScale from "../../utils/hasNumericScale.js"; 8 | import isOnNumericScale from "../../utils/isOnNumericScale.js"; 9 | import setValue from "../../utils/setValue.js"; 10 | 11 | const { 12 | createPlugin, 13 | utils: { report, validateOptions }, 14 | } = stylelint; 15 | 16 | const ruleName = "scales/line-heights"; 17 | const messages = createRuleMessages(ruleName); 18 | const meta = { 19 | url: "https://github.com/jeddy3/stylelint-scales/blob/main/lib/rules/line-heights/README.md", 20 | fixable: true, 21 | }; 22 | 23 | const propertyFilter = /^line-height$|^font$/; 24 | 25 | const rule = (primary) => { 26 | return (root, result) => { 27 | if ( 28 | !validateOptions(result, ruleName, { 29 | actual: primary, 30 | possible: hasNumericScale, 31 | }) 32 | ) 33 | return; 34 | 35 | root.walkDecls(propertyFilter, (decl) => { 36 | const { prop } = decl; 37 | const value = getValue(decl); 38 | 39 | const valueRoot = parse(value, { 40 | ignoreUnknownWords: true, 41 | }); 42 | 43 | switch (prop) { 44 | case "font": { 45 | const node = findLineHeight(valueRoot.nodes); 46 | if (node) check(node); 47 | break; 48 | } 49 | case "line-height": 50 | valueRoot.walkNumerics((node) => { 51 | check(node); 52 | }); 53 | break; 54 | } 55 | 56 | function check(node) { 57 | const { value, unit } = node; 58 | 59 | if (unit) return; 60 | 61 | if (isOnNumericScale(primary, value)) return; 62 | 63 | const fix = () => { 64 | node.value = getClosest(primary, value); 65 | setValue(decl, valueRoot.toString()); 66 | }; 67 | 68 | report({ 69 | fix, 70 | message: messages.expected(value, primary.join(", ")), 71 | node: decl, 72 | result, 73 | ruleName, 74 | word: value, 75 | }); 76 | } 77 | }); 78 | }; 79 | }; 80 | 81 | function findLineHeight(nodes) { 82 | const node = nodes.find( 83 | ({ type, value }) => type === "operator" && value === "/", 84 | ); 85 | if (node) return node.next(); 86 | } 87 | 88 | rule.primaryOptionArray = true; 89 | 90 | rule.ruleName = ruleName; 91 | rule.messages = messages; 92 | rule.meta = meta; 93 | 94 | export default createPlugin(ruleName, rule); 95 | -------------------------------------------------------------------------------- /lib/rules/line-heights/index.test.js: -------------------------------------------------------------------------------- 1 | import { testRule } from "stylelint-test-rule-node"; 2 | 3 | import plugin from "./index.js"; 4 | 5 | const { 6 | rule: { messages, ruleName }, 7 | } = plugin; 8 | 9 | testRule({ 10 | ruleName, 11 | config: [1, 2], 12 | fix: true, 13 | plugins: [plugin], 14 | 15 | accept: [ 16 | { 17 | code: "a { line-height: 1; }", 18 | description: "Value on scale", 19 | }, 20 | { 21 | code: "a { font: 400 1px/2 serif; }", 22 | description: "Value on scale in shorthand", 23 | }, 24 | { 25 | code: "a { font: 400 1px/3px serif; }", 26 | description: "Ignored unit", 27 | }, 28 | ], 29 | 30 | reject: [ 31 | { 32 | code: "a { line-height: 3 }", 33 | fixed: "a { line-height: 2 }", 34 | message: messages.expected("3", "1, 2"), 35 | line: 1, 36 | column: 18, 37 | description: "Value off scale", 38 | }, 39 | { 40 | code: "a { font: 700 3px/3 sans-serif; }", 41 | fixed: "a { font: 700 3px/2 sans-serif; }", 42 | message: messages.expected("3", "1, 2"), 43 | line: 1, 44 | column: 15, 45 | description: "Value off scale in shorthand", 46 | }, 47 | ], 48 | }); 49 | -------------------------------------------------------------------------------- /lib/rules/radii/README.md: -------------------------------------------------------------------------------- 1 | # radii 2 | 3 | Specify scales for radii. 4 | 5 | ```css 6 | a { 7 | border-radius: 2px; 8 | } 9 | /** ↑ 10 | * This radius */ 11 | ``` 12 | 13 | This rule can automatically fix all of the problems reported. 14 | 15 | This rule checks `` and `` values. 16 | 17 | This rule can be paired with the [`declaration-property-unit-allowed-list`](https://stylelint.io/user-guide/rules/declaration-property-unit-allowed-list) rule in stylelint, using the RegEx: 18 | 19 | ``` 20 | /radius$/ 21 | ``` 22 | 23 | ## Options 24 | 25 | `array` of `objects` as `{scale: [], units: []}` 26 | 27 | Given: 28 | 29 | ```json 30 | [{ "scale": [1, 2], "units": ["px"] }] 31 | ``` 32 | 33 | The following patterns are considered violations: 34 | 35 | ```css 36 | a { 37 | border-radius: 4px; 38 | } 39 | ``` 40 | 41 | The following patterns are _not_ considered violations: 42 | 43 | ```css 44 | a { 45 | border-top-right-radius: 2px; 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /lib/rules/radii/index.js: -------------------------------------------------------------------------------- 1 | import { parse } from "postcss-values-parser"; 2 | import stylelint from "stylelint"; 3 | 4 | import createRuleMessages from "../../utils/createRuleMessages.js"; 5 | import findScaleByUnit from "../../utils/findScaleByUnit.js"; 6 | import getClosest from "../../utils/getClosest.js"; 7 | import getValue from "../../utils/getValue.js"; 8 | import hasScalesWithUnits from "../../utils/hasScalesWithUnits.js"; 9 | import isOnNumericScale from "../../utils/isOnNumericScale.js"; 10 | import setValue from "../../utils/setValue.js"; 11 | 12 | const { 13 | createPlugin, 14 | utils: { report, validateOptions }, 15 | } = stylelint; 16 | 17 | const ruleName = "scales/radii"; 18 | const messages = createRuleMessages(ruleName); 19 | const meta = { 20 | url: "https://github.com/jeddy3/stylelint-scales/blob/main/lib/rules/radii/README.md", 21 | fixable: true, 22 | }; 23 | 24 | const propertyFilter = /radius$/; 25 | 26 | const rule = (primary) => { 27 | return (root, result) => { 28 | if ( 29 | !validateOptions(result, ruleName, { 30 | actual: primary, 31 | possible: hasScalesWithUnits, 32 | }) 33 | ) 34 | return; 35 | 36 | root.walkDecls(propertyFilter, (decl) => { 37 | const value = getValue(decl); 38 | 39 | const valueRoot = parse(value, { 40 | ignoreUnknownWords: true, 41 | }); 42 | 43 | valueRoot.walkNumerics((node) => { 44 | check(node); 45 | }); 46 | 47 | function check(node) { 48 | const { value, unit } = node; 49 | 50 | const scale = findScaleByUnit(primary, unit); 51 | 52 | if (isOnNumericScale(scale, value)) return; 53 | 54 | const fix = () => { 55 | node.value = getClosest(scale, value); 56 | setValue(decl, valueRoot.toString()); 57 | }; 58 | 59 | report({ 60 | fix, 61 | message: messages.expected(value, scale.join(", "), unit), 62 | node: decl, 63 | result, 64 | ruleName, 65 | word: value, 66 | }); 67 | } 68 | }); 69 | }; 70 | }; 71 | 72 | rule.primaryOptionArray = true; 73 | 74 | rule.ruleName = ruleName; 75 | rule.messages = messages; 76 | rule.meta = meta; 77 | 78 | export default createPlugin(ruleName, rule); 79 | -------------------------------------------------------------------------------- /lib/rules/radii/index.test.js: -------------------------------------------------------------------------------- 1 | import { testRule } from "stylelint-test-rule-node"; 2 | 3 | import plugin from "./index.js"; 4 | 5 | const { 6 | rule: { messages, ruleName }, 7 | } = plugin; 8 | 9 | testRule({ 10 | ruleName, 11 | config: [ 12 | { 13 | scale: [1, 2], 14 | units: ["px"], 15 | }, 16 | ], 17 | fix: true, 18 | plugins: [plugin], 19 | 20 | accept: [ 21 | { 22 | code: "a { border-radius: 1px; }", 23 | description: "Value on scale", 24 | }, 25 | { 26 | code: "a { border-top-right-radius: 1px; }", 27 | description: "Value on scale in longhand", 28 | }, 29 | { 30 | code: "a { border-radius: 1px 2px; }", 31 | description: "Multiple values on scale", 32 | }, 33 | { 34 | code: "a { border-bottom-right-radius: calc(100% - 2px); }", 35 | description: "Value on scale within calc", 36 | }, 37 | { 38 | code: "a { border-top-right-radius: 0; }", 39 | description: "Ignored unitless value", 40 | }, 41 | { 42 | code: "a { width: 3rem; }", 43 | description: "Ignored property", 44 | }, 45 | { 46 | code: "a { border-bottom-left-radius: 3vh; }", 47 | description: "Ignored unit", 48 | }, 49 | { 50 | code: "a { border-top-left-radius: 1px /* 3px */ }", 51 | description: "Ignored within comment", 52 | }, 53 | ], 54 | 55 | reject: [ 56 | { 57 | code: "a { border-radius: 3px }", 58 | fixed: "a { border-radius: 2px }", 59 | message: messages.expected("3", "1, 2", "px"), 60 | line: 1, 61 | column: 20, 62 | description: "Value off scale", 63 | }, 64 | { 65 | code: "a { border-radius: 0.5px 5px }", 66 | fixed: "a { border-radius: 1px 2px }", 67 | warnings: [ 68 | { 69 | message: messages.expected("0.5", "1, 2", "px"), 70 | line: 1, 71 | column: 20, 72 | }, 73 | { message: messages.expected("5", "1, 2", "px"), line: 1, column: 22 }, 74 | ], 75 | 76 | description: "Multiple values off scale", 77 | }, 78 | { 79 | code: "a { border-top-right-radius: 4px }", 80 | fixed: "a { border-top-right-radius: 2px }", 81 | message: messages.expected("4", "1, 2", "px"), 82 | line: 1, 83 | column: 30, 84 | description: "Value off scale in longhand", 85 | }, 86 | { 87 | code: "a { border-bottom-left-radius: calc(100% - 3px); }", 88 | fixed: "a { border-bottom-left-radius: calc(100% - 2px); }", 89 | message: messages.expected("3", "1, 2", "px"), 90 | line: 1, 91 | column: 44, 92 | description: "Value off scale within calc", 93 | }, 94 | ], 95 | }); 96 | -------------------------------------------------------------------------------- /lib/rules/sizes/README.md: -------------------------------------------------------------------------------- 1 | # sizes 2 | 3 | Specify scales for sizes. 4 | 5 | ```css 6 | a { 7 | width: 400px; 8 | } 9 | /** ↑ 10 | * This size */ 11 | ``` 12 | 13 | This rule can automatically fix all of the problems reported. 14 | 15 | This rule checks `` and `` values. 16 | 17 | This rule can be paired with the [`declaration-property-unit-allowed-list`](https://stylelint.io/user-guide/rules/declaration-property-unit-allowed-list) rule in stylelint, using the RegEx: 18 | 19 | ``` 20 | /^((min|max)-)?(height$|width$|block-size$|inline-size$)/ 21 | ``` 22 | 23 | ## Options 24 | 25 | `array` of `objects` as `{scale: [], units: []}` 26 | 27 | Given: 28 | 29 | ```json 30 | [ 31 | { 32 | "scale": [100, 150], 33 | "units": ["px"] 34 | } 35 | ] 36 | ``` 37 | 38 | The following patterns are considered violations: 39 | 40 | ```css 41 | a { 42 | width: 125px; 43 | } 44 | ``` 45 | 46 | ```css 47 | a { 48 | max-height: 200px; 49 | } 50 | ``` 51 | 52 | The following patterns are _not_ considered violations: 53 | 54 | ```css 55 | a { 56 | width: 100px; 57 | } 58 | ``` 59 | 60 | ```css 61 | a { 62 | max-height: 150px; 63 | } 64 | ``` 65 | -------------------------------------------------------------------------------- /lib/rules/sizes/index.js: -------------------------------------------------------------------------------- 1 | import { parse } from "postcss-values-parser"; 2 | import stylelint from "stylelint"; 3 | 4 | import createRuleMessages from "../../utils/createRuleMessages.js"; 5 | import findScaleByUnit from "../../utils/findScaleByUnit.js"; 6 | import getClosest from "../../utils/getClosest.js"; 7 | import getValue from "../../utils/getValue.js"; 8 | import hasScalesWithUnits from "../../utils/hasScalesWithUnits.js"; 9 | import isOnNumericScale from "../../utils/isOnNumericScale.js"; 10 | import setValue from "../../utils/setValue.js"; 11 | 12 | const { 13 | createPlugin, 14 | utils: { report, validateOptions }, 15 | } = stylelint; 16 | 17 | const ruleName = "scales/sizes"; 18 | const messages = createRuleMessages(ruleName); 19 | const meta = { 20 | url: "https://github.com/jeddy3/stylelint-scales/blob/main/lib/rules/sizes/README.md", 21 | fixable: true, 22 | }; 23 | 24 | const propertyFilter = 25 | /^((min|max)-)?(height$|width$|block-size$|inline-size$)/; 26 | 27 | const rule = (primary) => { 28 | return (root, result) => { 29 | if ( 30 | !validateOptions(result, ruleName, { 31 | actual: primary, 32 | possible: hasScalesWithUnits, 33 | }) 34 | ) 35 | return; 36 | 37 | root.walkDecls(propertyFilter, (decl) => { 38 | const value = getValue(decl); 39 | 40 | const valueRoot = parse(value, { 41 | ignoreUnknownWords: true, 42 | }); 43 | 44 | valueRoot.walkNumerics((node) => { 45 | check(node); 46 | }); 47 | 48 | function check(node) { 49 | const { value, unit } = node; 50 | 51 | const scale = findScaleByUnit(primary, unit); 52 | 53 | if (isOnNumericScale(scale, value)) return; 54 | 55 | const fix = () => { 56 | node.value = getClosest(scale, value); 57 | setValue(decl, valueRoot.toString()); 58 | }; 59 | 60 | report({ 61 | fix, 62 | message: messages.expected(value, scale.join(", "), unit), 63 | node: decl, 64 | result, 65 | ruleName, 66 | word: value, 67 | }); 68 | } 69 | }); 70 | }; 71 | }; 72 | 73 | rule.primaryOptionArray = true; 74 | 75 | rule.ruleName = ruleName; 76 | rule.messages = messages; 77 | rule.meta = meta; 78 | 79 | export default createPlugin(ruleName, rule); 80 | -------------------------------------------------------------------------------- /lib/rules/sizes/index.test.js: -------------------------------------------------------------------------------- 1 | import { testRule } from "stylelint-test-rule-node"; 2 | 3 | import plugin from "./index.js"; 4 | 5 | const { 6 | rule: { messages, ruleName }, 7 | } = plugin; 8 | 9 | testRule({ 10 | ruleName, 11 | config: [ 12 | { 13 | scale: [100, 200], 14 | units: ["px"], 15 | }, 16 | ], 17 | fix: true, 18 | plugins: [plugin], 19 | 20 | accept: [ 21 | { 22 | code: "a { height: 100px; }", 23 | description: "Value on scale", 24 | }, 25 | { 26 | code: "a { max-height: 200px; }", 27 | description: "Value on scale in max", 28 | }, 29 | { 30 | code: "a { border-image-width: 150px; }", 31 | description: "Ignored width property", 32 | }, 33 | { 34 | code: "a { font-size: 150px; }", 35 | description: "Ignored size property", 36 | }, 37 | ], 38 | 39 | reject: [ 40 | { 41 | code: "a { width: 120px }", 42 | fixed: "a { width: 100px }", 43 | message: messages.expected("120", "100, 200", "px"), 44 | line: 1, 45 | column: 12, 46 | description: "Value off scale", 47 | }, 48 | { 49 | code: "a { max-width: 300px }", 50 | fixed: "a { max-width: 200px }", 51 | message: messages.expected("300", "100, 200", "px"), 52 | line: 1, 53 | column: 16, 54 | description: "Value off scale in max", 55 | }, 56 | { 57 | code: "a { block-size: 120px }", 58 | fixed: "a { block-size: 100px }", 59 | message: messages.expected("120", "100, 200", "px"), 60 | line: 1, 61 | column: 17, 62 | description: "Value off scale for logical", 63 | }, 64 | { 65 | code: "a { min-inline-size: 50px }", 66 | fixed: "a { min-inline-size: 100px }", 67 | message: messages.expected("50", "100, 200", "px"), 68 | line: 1, 69 | column: 22, 70 | description: "Value off scale for min logical", 71 | }, 72 | ], 73 | }); 74 | -------------------------------------------------------------------------------- /lib/rules/space/README.md: -------------------------------------------------------------------------------- 1 | # space 2 | 3 | Specify scales for space. 4 | 5 | ```css 6 | a { 7 | margin: 1rem; 8 | } 9 | /** ↑ 10 | * This space */ 11 | ``` 12 | 13 | This rule can automatically fix all of the problems reported. 14 | 15 | This rule checks `` and `` values. 16 | 17 | This rule can be paired with the [`declaration-property-unit-allowed-list`](https://stylelint.io/user-guide/rules/declaration-property-unit-allowed-list) rule in stylelint, using the RegEx: 18 | 19 | ``` 20 | /^inset|gap|^margin|^padding/ 21 | ``` 22 | 23 | ## Options 24 | 25 | `array` of `objects` as `{scale: [], units: []}` 26 | 27 | Negative spaces on the scale are implicitly allowed. 28 | 29 | Given: 30 | 31 | ```json 32 | [ 33 | { "scale": [1, 2], "units": ["em", "rem"] }, 34 | { "scale": [16, 32], "units": ["px"] } 35 | ] 36 | ``` 37 | 38 | The following patterns are considered violations: 39 | 40 | ```css 41 | a { 42 | margin: 16rem; 43 | } 44 | ``` 45 | 46 | ```css 47 | a { 48 | padding: 2px; 49 | } 50 | ``` 51 | 52 | The following patterns are _not_ considered violations: 53 | 54 | ```css 55 | a { 56 | margin: 1em; 57 | } 58 | ``` 59 | 60 | ```css 61 | a { 62 | padding: 2rem; 63 | } 64 | ``` 65 | 66 | ```css 67 | a { 68 | gap: -32px; 69 | } 70 | ``` 71 | 72 | ## Optional secondary options 73 | 74 | ### `ignoreFunctionArguments: { "function-name": [] }` 75 | 76 | Given: 77 | 78 | ```json 79 | [ 80 | [{ "scale": [1, 2], "units": ["rem"] }], 81 | { 82 | "ignoreFunctionArguments": { "clamp": [1], "min": [0, 1] } 83 | } 84 | ] 85 | ``` 86 | 87 | The following patterns are _not_ considered problems: 88 | 89 | ```css 90 | a { 91 | gap: clamp(1rem, 0.37rem + 0.45vw, 2rem); 92 | } 93 | ``` 94 | 95 | ```css 96 | a { 97 | gap: min(3rem, 4rem); 98 | } 99 | ``` 100 | -------------------------------------------------------------------------------- /lib/rules/space/index.js: -------------------------------------------------------------------------------- 1 | import { parse } from "postcss-values-parser"; 2 | import stylelint from "stylelint"; 3 | 4 | import createRuleMessages from "../../utils/createRuleMessages.js"; 5 | import findScaleByUnit from "../../utils/findScaleByUnit.js"; 6 | import getClosest from "../../utils/getClosest.js"; 7 | import getValue from "../../utils/getValue.js"; 8 | import hasScalesWithUnits from "../../utils/hasScalesWithUnits.js"; 9 | import hasObjectWithNumericArray from "../../utils/hasObjectWithNumericArray.js"; 10 | import isIgnoredFunctionArgument from "../../utils/isIgnoredFunctionArgument.js"; 11 | import isOnNumericScale from "../../utils/isOnNumericScale.js"; 12 | import setValue from "../../utils/setValue.js"; 13 | 14 | const { 15 | createPlugin, 16 | utils: { report, validateOptions }, 17 | } = stylelint; 18 | 19 | const ruleName = "scales/space"; 20 | const messages = createRuleMessages(ruleName); 21 | const meta = { 22 | url: "https://github.com/jeddy3/stylelint-scales/blob/main/lib/rules/space/README.md", 23 | fixable: true, 24 | }; 25 | 26 | const propertyFilter = /^inset|gap|^margin|^padding/; 27 | 28 | const rule = (primary, secondary) => { 29 | return (root, result) => { 30 | if ( 31 | !validateOptions( 32 | result, 33 | ruleName, 34 | { 35 | actual: primary, 36 | possible: hasScalesWithUnits, 37 | }, 38 | { 39 | optional: true, 40 | actual: secondary, 41 | possible: { 42 | ignoreFunctionArguments: hasObjectWithNumericArray, 43 | }, 44 | }, 45 | ) 46 | ) 47 | return; 48 | 49 | const ignoreFunctionArguments = secondary?.ignoreFunctionArguments; 50 | 51 | root.walkDecls(propertyFilter, (decl) => { 52 | const value = getValue(decl); 53 | 54 | const valueRoot = parse(value, { 55 | ignoreUnknownWords: true, 56 | }); 57 | 58 | valueRoot.walkNumerics((node) => { 59 | check(node); 60 | }); 61 | 62 | function check(node) { 63 | const { value, unit } = node; 64 | const absoluteValue = Math.abs(value); 65 | 66 | if (isIgnoredFunctionArgument(node, ignoreFunctionArguments)) return; 67 | 68 | const scale = findScaleByUnit(primary, unit); 69 | 70 | if (isOnNumericScale(scale, absoluteValue)) return; 71 | 72 | const fix = () => { 73 | const closest = getClosest(scale, absoluteValue); 74 | node.value = value >= 0 ? closest : -closest; 75 | setValue(decl, valueRoot.toString()); 76 | }; 77 | 78 | report({ 79 | fix, 80 | message: messages.expected(absoluteValue, scale.join(", "), unit), 81 | node: decl, 82 | result, 83 | ruleName, 84 | word: value, 85 | }); 86 | } 87 | }); 88 | }; 89 | }; 90 | 91 | rule.primaryOptionArray = true; 92 | 93 | rule.ruleName = ruleName; 94 | rule.messages = messages; 95 | rule.meta = meta; 96 | 97 | export default createPlugin(ruleName, rule); 98 | -------------------------------------------------------------------------------- /lib/rules/space/index.test.js: -------------------------------------------------------------------------------- 1 | import { testRule } from "stylelint-test-rule-node"; 2 | 3 | import plugin from "./index.js"; 4 | 5 | const { 6 | rule: { messages, ruleName }, 7 | } = plugin; 8 | 9 | testRule({ 10 | ruleName, 11 | config: [ 12 | { 13 | scale: [1.5, 2.25], 14 | units: ["em", "rem"], 15 | }, 16 | { 17 | scale: [3, 4], 18 | units: ["px"], 19 | }, 20 | ], 21 | fix: true, 22 | plugins: [plugin], 23 | 24 | accept: [ 25 | { 26 | code: "a { margin: 1.5rem; }", 27 | description: "Value on scale", 28 | }, 29 | { 30 | code: "a { margin: -1.5rem; }", 31 | description: "Negative value on scale", 32 | }, 33 | { 34 | code: "a { margin: 1.5rem 2.25rem; }", 35 | description: "Multiple values on scale", 36 | }, 37 | { 38 | code: "a { margin: calc(100% - 2.25rem); }", 39 | description: "Value on scale within calc", 40 | }, 41 | { 42 | code: "a { margin: 0; }", 43 | description: "Ignored unitless value", 44 | }, 45 | { 46 | code: "a { margin: 1ch; }", 47 | description: "Ignored unit", 48 | }, 49 | { 50 | code: "a { width: 3rem; }", 51 | description: "Ignored property", 52 | }, 53 | { 54 | code: "a { margin: 1.5rem /* 3rem */ }", 55 | description: "Ignored in comment", 56 | }, 57 | ], 58 | 59 | reject: [ 60 | { 61 | code: "a { padding: 2rem }", 62 | fixed: "a { padding: 2.25rem }", 63 | message: messages.expected("2", "1.5, 2.25", "rem"), 64 | line: 1, 65 | column: 14, 66 | description: "Value off scale", 67 | }, 68 | { 69 | code: "a { padding: -2rem }", 70 | fixed: "a { padding: -2.25rem }", 71 | message: messages.expected("2", "1.5, 2.25", "rem"), 72 | line: 1, 73 | column: 14, 74 | description: "Negative value off scale", 75 | }, 76 | { 77 | code: "a { row-gap: 5px }", 78 | fixed: "a { row-gap: 4px }", 79 | message: messages.expected("5", "3, 4", "px"), 80 | line: 1, 81 | column: 14, 82 | description: "Value off scale using gap property", 83 | }, 84 | { 85 | code: "a { inset-block-end: 5px }", 86 | fixed: "a { inset-block-end: 4px }", 87 | message: messages.expected("5", "3, 4", "px"), 88 | line: 1, 89 | column: 22, 90 | description: "Value off scale for inset longhand", 91 | }, 92 | { 93 | code: "a { margin-inline-start: 2px }", 94 | fixed: "a { margin-inline-start: 3px }", 95 | message: messages.expected("2", "3, 4", "px"), 96 | line: 1, 97 | column: 26, 98 | description: "Value off scale for logical margin", 99 | }, 100 | { 101 | code: "a { padding: 0.5px /* comment */ 1rem }", 102 | fixed: "a { padding: 3px /* comment */ 1.5rem }", 103 | warnings: [ 104 | { 105 | message: messages.expected("0.5", "3, 4", "px"), 106 | line: 1, 107 | column: 14, 108 | }, 109 | { 110 | message: messages.expected("1", "1.5, 2.25", "rem"), 111 | line: 1, 112 | column: 34, 113 | }, 114 | ], 115 | description: "Values off scale and split by comment", 116 | }, 117 | ], 118 | }); 119 | 120 | testRule({ 121 | ruleName, 122 | config: [ 123 | [ 124 | { 125 | scale: [1, 2], 126 | units: ["rem"], 127 | }, 128 | ], 129 | { 130 | ignoreFunctionArguments: { 131 | clamp: [1], 132 | min: [0], 133 | max: [0, 1], 134 | }, 135 | }, 136 | ], 137 | fix: true, 138 | plugins: [plugin], 139 | 140 | accept: [ 141 | { 142 | code: "a { gap: 1rem; }", 143 | description: "Value on scale", 144 | }, 145 | { 146 | code: "a { gap: clamp(1rem, 0.5rem + 0.5vw, 2rem); }", 147 | description: "Value off scale for ignored argument", 148 | }, 149 | { 150 | code: "a { gap: min(0.5rem, 1rem); }", 151 | description: "Value off scale for ignored argument", 152 | }, 153 | { 154 | code: "a { gap: max(0.5rem, 2.5rem); }", 155 | description: "Two values off scale both ignored", 156 | }, 157 | ], 158 | 159 | reject: [ 160 | { 161 | code: "a { gap: 3rem; }", 162 | fixed: "a { gap: 2rem; }", 163 | message: messages.expected("3", "1, 2", "rem"), 164 | line: 1, 165 | column: 10, 166 | description: "Value off scale", 167 | }, 168 | { 169 | code: "a { gap: min(0.5rem, 2.5rem); }", 170 | fixed: "a { gap: min(0.5rem, 2rem); }", 171 | message: messages.expected("2.5", "1, 2", "rem"), 172 | line: 1, 173 | column: 22, 174 | description: "Two values off scale with one ignored", 175 | }, 176 | { 177 | code: "a { gap: clamp(0.5rem, 0.3rem + 0.5vw, 2.5rem); }", 178 | fixed: "a { gap: clamp(1rem, 0.3rem + 0.5vw, 2rem); }", 179 | warnings: [ 180 | { 181 | message: messages.expected("0.5", "1, 2", "rem"), 182 | line: 1, 183 | column: 16, 184 | }, 185 | { 186 | message: messages.expected("2.5", "1, 2", "rem"), 187 | line: 1, 188 | column: 40, 189 | }, 190 | ], 191 | description: "Two values off scale", 192 | }, 193 | ], 194 | }); 195 | -------------------------------------------------------------------------------- /lib/rules/word-spacings/README.md: -------------------------------------------------------------------------------- 1 | # word-spacings 2 | 3 | Specify scales for word spacings. 4 | 5 | ```css 6 | a { 7 | word-spacing: 0.1rem; 8 | } 9 | /** ↑ 10 | * This word spacing */ 11 | ``` 12 | 13 | This rule can automatically fix all of the problems reported. 14 | 15 | This rule checks `` and `` values. 16 | 17 | This rule can be paired with the [`declaration-property-unit-allowed-list`](https://stylelint.io/user-guide/rules/declaration-property-unit-allowed-list) rule in stylelint. 18 | 19 | ## Options 20 | 21 | `array` of `objects` as `{scale: [], units: []}` 22 | 23 | Given: 24 | 25 | ```json 26 | [ 27 | { 28 | "scale": [-0.1, 0.2], 29 | "units": ["rem"] 30 | } 31 | ] 32 | ``` 33 | 34 | The following patterns are considered violations: 35 | 36 | ```css 37 | a { 38 | word-spacing: 0.1rem; 39 | } 40 | ``` 41 | 42 | ```css 43 | a { 44 | word-spacing: 0.5rem; 45 | } 46 | ``` 47 | 48 | The following patterns are _not_ considered violations: 49 | 50 | ```css 51 | a { 52 | word-spacing: -0.1rem; 53 | } 54 | ``` 55 | 56 | ```css 57 | a { 58 | word-spacing: 0.2rem; 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /lib/rules/word-spacings/index.js: -------------------------------------------------------------------------------- 1 | import { parse } from "postcss-values-parser"; 2 | import stylelint from "stylelint"; 3 | 4 | import createRuleMessages from "../../utils/createRuleMessages.js"; 5 | import findScaleByUnit from "../../utils/findScaleByUnit.js"; 6 | import getClosest from "../../utils/getClosest.js"; 7 | import getValue from "../../utils/getValue.js"; 8 | import hasScalesWithUnits from "../../utils/hasScalesWithUnits.js"; 9 | import isOnNumericScale from "../../utils/isOnNumericScale.js"; 10 | import setValue from "../../utils/setValue.js"; 11 | 12 | const { 13 | createPlugin, 14 | utils: { report, validateOptions }, 15 | } = stylelint; 16 | 17 | const ruleName = "scales/word-spacings"; 18 | const messages = createRuleMessages(ruleName); 19 | const meta = { 20 | url: "https://github.com/jeddy3/stylelint-scales/blob/main/lib/rules/word-spacings/README.md", 21 | fixable: true, 22 | }; 23 | 24 | const propertyFilter = "word-spacing"; 25 | 26 | const rule = (primary) => { 27 | return (root, result) => { 28 | if ( 29 | !validateOptions(result, ruleName, { 30 | actual: primary, 31 | possible: hasScalesWithUnits, 32 | }) 33 | ) 34 | return; 35 | 36 | root.walkDecls(propertyFilter, (decl) => { 37 | const value = getValue(decl); 38 | 39 | const valueRoot = parse(value, { 40 | ignoreUnknownWords: true, 41 | }); 42 | 43 | valueRoot.walkNumerics((node) => { 44 | check(node); 45 | }); 46 | 47 | function check(node) { 48 | const { value, unit } = node; 49 | 50 | const scale = findScaleByUnit(primary, unit); 51 | 52 | if (isOnNumericScale(scale, value)) return; 53 | 54 | const fix = () => { 55 | node.value = getClosest(scale, value); 56 | setValue(decl, valueRoot.toString()); 57 | }; 58 | 59 | report({ 60 | fix, 61 | message: messages.expected(value, scale.join(", "), unit), 62 | node: decl, 63 | result, 64 | ruleName, 65 | word: value, 66 | }); 67 | } 68 | }); 69 | }; 70 | }; 71 | 72 | rule.primaryOptionArray = true; 73 | 74 | rule.ruleName = ruleName; 75 | rule.messages = messages; 76 | rule.meta = meta; 77 | 78 | export default createPlugin(ruleName, rule); 79 | -------------------------------------------------------------------------------- /lib/rules/word-spacings/index.test.js: -------------------------------------------------------------------------------- 1 | import { testRule } from "stylelint-test-rule-node"; 2 | 3 | import plugin from "./index.js"; 4 | 5 | const { 6 | rule: { messages, ruleName }, 7 | } = plugin; 8 | 9 | testRule({ 10 | ruleName, 11 | config: [ 12 | { 13 | scale: [-1, 2], 14 | units: ["px"], 15 | }, 16 | ], 17 | fix: true, 18 | plugins: [plugin], 19 | 20 | accept: [ 21 | { 22 | code: "a { word-spacing: -1px; }", 23 | description: "Value on scale", 24 | }, 25 | { 26 | code: "a { width: 3px; }", 27 | description: "Ignored property", 28 | }, 29 | { 30 | code: "a { word-spacing: 3vh; }", 31 | description: "Ignored unit", 32 | }, 33 | ], 34 | 35 | reject: [ 36 | { 37 | code: "a { word-spacing: 3px }", 38 | fixed: "a { word-spacing: 2px }", 39 | message: messages.expected("3", "-1, 2", "px"), 40 | line: 1, 41 | column: 19, 42 | description: "Value off scale", 43 | }, 44 | { 45 | code: "a { word-spacing: -0.5px }", 46 | fixed: "a { word-spacing: -1px }", 47 | message: messages.expected("-0.5", "-1, 2", "px"), 48 | line: 1, 49 | column: 19, 50 | description: "Negative value off scale", 51 | }, 52 | ], 53 | }); 54 | -------------------------------------------------------------------------------- /lib/rules/z-indices/README.md: -------------------------------------------------------------------------------- 1 | # z-indices 2 | 3 | Specify a scale for z-indices. 4 | 5 | ```css 6 | a { 7 | z-index: 1; 8 | } 9 | /** ↑ 10 | * This z-index */ 11 | ``` 12 | 13 | This rule can automatically fix all of the problems reported. 14 | 15 | ## Options 16 | 17 | `array` 18 | 19 | Given: 20 | 21 | ```json 22 | [1, 2] 23 | ``` 24 | 25 | The following patterns are considered violations: 26 | 27 | ```css 28 | a { 29 | z-index: 35; 30 | } 31 | ``` 32 | 33 | The following patterns are _not_ considered violations: 34 | 35 | ```css 36 | a { 37 | z-index: 1; 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /lib/rules/z-indices/index.js: -------------------------------------------------------------------------------- 1 | import { parse } from "postcss-values-parser"; 2 | import stylelint from "stylelint"; 3 | 4 | import createRuleMessages from "../../utils/createRuleMessages.js"; 5 | import getClosest from "../../utils/getClosest.js"; 6 | import getValue from "../../utils/getValue.js"; 7 | import hasNumericScale from "../../utils/hasNumericScale.js"; 8 | import isOnNumericScale from "../../utils/isOnNumericScale.js"; 9 | import setValue from "../../utils/setValue.js"; 10 | 11 | const { 12 | createPlugin, 13 | utils: { report, validateOptions }, 14 | } = stylelint; 15 | 16 | const ruleName = "scales/z-indices"; 17 | const messages = createRuleMessages(ruleName); 18 | const meta = { 19 | url: "https://github.com/jeddy3/stylelint-scales/blob/main/lib/rules/z-indices/README.md", 20 | fixable: true, 21 | }; 22 | 23 | const propertyFilter = "z-index"; 24 | 25 | const rule = (primary) => { 26 | return (root, result) => { 27 | if ( 28 | !validateOptions(result, ruleName, { 29 | actual: primary, 30 | possible: hasNumericScale, 31 | }) 32 | ) 33 | return; 34 | 35 | root.walkDecls(propertyFilter, (decl) => { 36 | const value = getValue(decl); 37 | 38 | const valueRoot = parse(value, { 39 | ignoreUnknownWords: true, 40 | }); 41 | 42 | valueRoot.walkNumerics((node) => { 43 | check(node); 44 | }); 45 | 46 | function check(node) { 47 | const { value, unit } = node; 48 | 49 | if (unit) return; 50 | 51 | if (isOnNumericScale(primary, value)) return; 52 | 53 | const fix = () => { 54 | node.value = getClosest(primary, value); 55 | setValue(decl, valueRoot.toString()); 56 | }; 57 | 58 | report({ 59 | fix, 60 | message: messages.expected(value, primary.join(", ")), 61 | node: decl, 62 | result, 63 | ruleName, 64 | word: value, 65 | }); 66 | } 67 | }); 68 | }; 69 | }; 70 | 71 | rule.primaryOptionArray = true; 72 | 73 | rule.ruleName = ruleName; 74 | rule.messages = messages; 75 | rule.meta = meta; 76 | 77 | export default createPlugin(ruleName, rule); 78 | -------------------------------------------------------------------------------- /lib/rules/z-indices/index.test.js: -------------------------------------------------------------------------------- 1 | import { testRule } from "stylelint-test-rule-node"; 2 | 3 | import plugin from "./index.js"; 4 | 5 | const { 6 | rule: { messages, ruleName }, 7 | } = plugin; 8 | 9 | testRule({ 10 | ruleName, 11 | config: [-1, 1, 2], 12 | fix: true, 13 | plugins: [plugin], 14 | 15 | accept: [ 16 | { 17 | code: "a { z-index: 1 }", 18 | description: "Value on scale", 19 | }, 20 | { 21 | code: "a { z-index: -1 }", 22 | description: "Negative value on scale", 23 | }, 24 | { 25 | code: "a { border-width: 1px }", 26 | description: "Ignored property", 27 | }, 28 | ], 29 | 30 | reject: [ 31 | { 32 | code: "a { z-index: 3 }", 33 | fixed: "a { z-index: 2 }", 34 | message: messages.expected("3", "-1, 1, 2"), 35 | line: 1, 36 | column: 14, 37 | description: "Value off scale", 38 | }, 39 | { 40 | code: "a { z-index: -2 }", 41 | fixed: "a { z-index: -1 }", 42 | message: messages.expected("-2", "-1, 1, 2"), 43 | line: 1, 44 | column: 14, 45 | description: "Negative value off scale", 46 | }, 47 | { 48 | code: "a { z-index: 0.5 }", 49 | fixed: "a { z-index: 1 }", 50 | message: messages.expected("0.5", "-1, 1, 2"), 51 | line: 1, 52 | column: 14, 53 | description: "Factional value off scale", 54 | }, 55 | ], 56 | }); 57 | -------------------------------------------------------------------------------- /lib/utils/createRuleMessages.js: -------------------------------------------------------------------------------- 1 | import stylelint from "stylelint"; 2 | 3 | const { 4 | utils: { ruleMessages }, 5 | } = stylelint; 6 | 7 | /** 8 | * Create rule messages with value, scale and unit 9 | * 10 | * @param ruleName string 11 | * @return function 12 | */ 13 | export default function createRuleMessages(ruleName) { 14 | return ruleMessages(ruleName, { 15 | expected: (value, scale, unit) => 16 | `Expected "${value}" to be one of "${scale}"${ 17 | unit ? ` for "${unit}" unit` : "" 18 | }`, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /lib/utils/findScaleByUnit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Find scale by unit in option 3 | * 4 | * @param option array 5 | * @param unit string 6 | * @return array | void 7 | */ 8 | export default function findScaleByUnit(option, unit) { 9 | const match = option.find((scales) => 10 | scales.units.some((scaleUnit) => scaleUnit === unit), 11 | ); 12 | if (match) return match.scale; 13 | } 14 | -------------------------------------------------------------------------------- /lib/utils/findScaleByUnit.test.js: -------------------------------------------------------------------------------- 1 | import { test } from "node:test"; 2 | import { strict as assert } from "node:assert"; 3 | 4 | import hasScalesWithUnits from "./hasScalesWithUnits.js"; 5 | 6 | test("rejects strings", () => { 7 | assert.equal(hasScalesWithUnits("string"), false); 8 | }); 9 | 10 | test("rejects objects", () => { 11 | assert.equal(hasScalesWithUnits({}), false); 12 | }); 13 | 14 | test("rejects empty arrays", () => { 15 | assert.equal(hasScalesWithUnits([]), false); 16 | }); 17 | 18 | test("rejects empty objects", () => { 19 | assert.equal(hasScalesWithUnits([{}]), false); 20 | }); 21 | 22 | test("rejects missing units property", () => { 23 | assert.equal(hasScalesWithUnits([{ scales: [0] }]), false); 24 | }); 25 | 26 | test("rejects missing scales property", () => { 27 | assert.equal(hasScalesWithUnits([{ units: ["px"] }]), false); 28 | }); 29 | 30 | test("rejects string instead of array", () => { 31 | assert.equal(hasScalesWithUnits([{ units: "px", scale: [0] }]), false); 32 | }); 33 | 34 | test("rejects non-numeric scale values", () => { 35 | assert.equal(hasScalesWithUnits([{ units: ["px"], scale: ["0"] }]), false); 36 | }); 37 | 38 | test("rejects non-string unit values", () => { 39 | assert.equal(hasScalesWithUnits([{ units: [0], scale: [0] }]), false); 40 | }); 41 | 42 | test("accepts single value arrays", () => { 43 | assert.equal(hasScalesWithUnits([{ units: ["px"], scale: [0] }]), true); 44 | }); 45 | 46 | test("accepts multiple value arrays", () => { 47 | assert.equal( 48 | hasScalesWithUnits([{ units: ["px", "em"], scale: [0, 10] }]), 49 | true, 50 | ); 51 | }); 52 | -------------------------------------------------------------------------------- /lib/utils/getClosest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the closest number in an scale 3 | * 4 | * @param scale array 5 | * @param actual number 6 | * @return number 7 | */ 8 | export default function (scale, actual) { 9 | return scale.reduce(function (prev, curr) { 10 | return Math.abs(curr - actual) < Math.abs(prev - actual) ? curr : prev; 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /lib/utils/getValue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the value from a decl node 3 | * Getting from `raw` when neccessary to ensure comments are included 4 | * 5 | * @param decl Node 6 | * @return string 7 | */ 8 | export default function getValue(decl) { 9 | return decl.raws.value ? decl.raws.value.raw : decl.value; 10 | } 11 | -------------------------------------------------------------------------------- /lib/utils/hasNumericScale.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if primary option is a numeric scale 3 | * 4 | * @param primary array 5 | * @return bool 6 | */ 7 | export default function hasNumericScale(primary) { 8 | return ( 9 | Array.isArray(primary) && 10 | primary.length > 0 && 11 | primary.every((value) => Number.isFinite(value)) 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /lib/utils/hasNumericScale.test.js: -------------------------------------------------------------------------------- 1 | import { test } from "node:test"; 2 | import { strict as assert } from "node:assert"; 3 | 4 | import hasNumericScale from "./hasNumericScale.js"; 5 | 6 | test("rejects strings", () => { 7 | assert.equal(hasNumericScale("1"), false); 8 | }); 9 | 10 | test("rejects objects", () => { 11 | assert.equal(hasNumericScale({}), false); 12 | }); 13 | 14 | test("rejects empty arrays", () => { 15 | assert.equal(hasNumericScale([]), false); 16 | }); 17 | 18 | test("rejects string values in arrays", () => { 19 | assert.equal(hasNumericScale(["0"]), false); 20 | }); 21 | 22 | test("accepts single value arrays", () => { 23 | assert.equal(hasNumericScale([0]), true); 24 | }); 25 | 26 | test("accepts multiple value arrays", () => { 27 | assert.equal(hasNumericScale([0, 1]), true); 28 | }); 29 | -------------------------------------------------------------------------------- /lib/utils/hasObjectWithNumericArray.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if secondary option objects with numeric array 3 | * 4 | * @param secondary object 5 | * @return bool 6 | */ 7 | export default function hasObjectWithNumericArray(secondary) { 8 | return ( 9 | typeof secondary === "object" && 10 | Object.values(secondary).every((array) => 11 | array.every((item) => Number.isInteger(item)), 12 | ) 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /lib/utils/hasScalesWithUnits.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if option is an array of scales with units 3 | * 4 | * @param primary array 5 | * @return bool 6 | */ 7 | export default function hasScalesWithUnits(primary) { 8 | return ( 9 | Array.isArray(primary) && 10 | primary.length > 0 && 11 | primary.every( 12 | (obj) => 13 | typeof obj === "object" && 14 | "scale" in obj && 15 | "units" in obj && 16 | Array.isArray(obj.scale) && 17 | obj.scale.length > 0 && 18 | obj.scale.every((value) => Number.isFinite(value)) && 19 | Array.isArray(obj.units) && 20 | obj.units.length > 0 && 21 | obj.units.every((value) => typeof value === "string"), 22 | ) 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /lib/utils/isIgnoredFunctionArgument.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if a node is an ignored function argument 3 | * 4 | * @param node node 5 | * @return ignoreFunctionArguments object 6 | */ 7 | export default function isIgnoredFunctionArgument( 8 | node, 9 | ignoreFunctionArguments, 10 | ) { 11 | if (ignoreFunctionArguments && node?.parent?.type === "func") { 12 | const { parent } = node; 13 | const { name, nodes } = parent; 14 | const ignoredArgs = ignoreFunctionArguments[name]; 15 | const args = getArgs(nodes); 16 | 17 | if ( 18 | ignoredArgs && 19 | ignoredArgs.some((arg) => args[arg].some((n) => n === node)) 20 | ) 21 | return true; 22 | } 23 | 24 | return false; 25 | } 26 | 27 | function getArgs(nodes) { 28 | const args = []; 29 | let numerics = []; 30 | for (const node of nodes) { 31 | if (node.type === "punctuation" && node.value === ",") { 32 | args.push(numerics); 33 | numerics = []; 34 | } else { 35 | numerics.push(node); 36 | } 37 | } 38 | args.push(numerics); 39 | return args; 40 | } 41 | -------------------------------------------------------------------------------- /lib/utils/isLineHeight.js: -------------------------------------------------------------------------------- 1 | import isSlash from "./isSlash.js"; 2 | 3 | /** 4 | * Check if a node represents a line height 5 | * 6 | * @param node Node 7 | * @return bool 8 | */ 9 | export default function isLineHeight(node) { 10 | const previousNode = node.prev(); 11 | return previousNode && isSlash(previousNode); 12 | } 13 | -------------------------------------------------------------------------------- /lib/utils/isOnNumericScale.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if number is on a scale 3 | * 4 | * @param scale array 5 | * @param value number 6 | * @return bool 7 | */ 8 | export default function isOnNumericScale(scale, number) { 9 | return scale === undefined || scale.includes(Number(number)); 10 | } 11 | -------------------------------------------------------------------------------- /lib/utils/isSlash.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if a node is a slash 3 | * 4 | * @param node Node 5 | * @return bool 6 | */ 7 | export default function isSlash({ type, value }) { 8 | return type === "operator" && value === "/"; 9 | } 10 | -------------------------------------------------------------------------------- /lib/utils/setValue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Set the value of a decl node 3 | * Setting `raw` when neccessary to ensure comments are included 4 | * 5 | * @param decl Node 6 | * @param value string 7 | * @return null 8 | */ 9 | export default function setValue(decl, value) { 10 | if (decl.raws.value) decl.raws.value.raw = value; 11 | else decl.value = value; 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stylelint-scales", 3 | "description": "A Stylelint plugin pack to enforce numeric scales", 4 | "version": "5.0.0", 5 | "license": "MIT", 6 | "exports": "./lib/index.js", 7 | "files": [ 8 | "lib/**/*.js", 9 | "!lib/**/*.test.js" 10 | ], 11 | "type": "module", 12 | "repository": "jeddy3/stylelint-scales", 13 | "keywords": [ 14 | "stylelint", 15 | "stylelint-plugin", 16 | "spacing", 17 | "scales", 18 | "linting", 19 | "linter", 20 | "design-system", 21 | "type-scale" 22 | ], 23 | "engines": { 24 | "node": ">=18" 25 | }, 26 | "scripts": { 27 | "test": "node --test", 28 | "check": "npm-run-all --parallel check:*", 29 | "check:js": "eslint \"**/*.js\"", 30 | "check:formating": "prettier \"**/*.{js,md,yml}\" --check", 31 | "fix": "npm-run-all --sequential fix:*", 32 | "fix:js": "eslint \"**/*.js\" --fix", 33 | "fix:formating": "prettier \"**/*.{js,md,yml}\" --write", 34 | "release": "np" 35 | }, 36 | "dependencies": { 37 | "postcss-values-parser": "^6.0.2" 38 | }, 39 | "devDependencies": { 40 | "common-tags": "^1.8.2", 41 | "eslint": "^9.9.1", 42 | "np": "^10.0.7", 43 | "npm-run-all2": "^6.2.2", 44 | "prettier": "^3.3.3", 45 | "stylelint": "^16.9.0", 46 | "stylelint-test-rule-node": "^0.3.0" 47 | } 48 | } 49 | --------------------------------------------------------------------------------