├── .eslintrc.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── main.yaml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── commitlint.config.js ├── jest.config.js ├── jest.setup.js ├── package-lock.json ├── package.json └── src ├── index.js ├── rules ├── use-logical-properties-and-values │ ├── base.js │ ├── index.js │ └── index.test.js └── use-logical-units │ ├── base.js │ ├── index.js │ └── index.test.js └── utils ├── isPhysicalProperty.js ├── isPhysicalUnit.js ├── isPhysicalValue.js ├── logical.js ├── physical.js ├── physicalPropertiesMap.js ├── physicalUnitsMap.js ├── physicalValuesMap.js └── vendorPrefixes.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "overrides": [], 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module" 12 | }, 13 | "rules": {} 14 | } 15 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: yuschick 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 📒 Description 2 | 3 | > Write a general description and summary of the changes 4 | 5 | ## 🚀 Changes 6 | 7 | > List of changes this pull request includes 8 | 9 | 14 | 15 | ## 🔐 Closes 16 | 17 | > Include a link to a specific Github issue this pull request closes. 18 | 19 | ## ⛳️ Testing 20 | 21 | > List of tests completed to verify the change 22 | 23 | 27 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | branches: ['main'] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Install Dependencies 18 | run: yarn 19 | 20 | - name: Run Plugin Tests 21 | run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .DS_Store -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "printWidth": 80, 4 | "proseWrap": "always", 5 | "semi": true, 6 | "singleQuote": true, 7 | "tabWidth": 2, 8 | "trailingComma": "all" 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Fedya Petrakov 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 Plugin Logical CSS 2 | 3 | ![License](https://img.shields.io/github/license/yuschick/stylelint-plugin-logical-css?style=for-the-badge) 4 | ![NPM Version](https://img.shields.io/npm/v/stylelint-plugin-logical-css?style=for-the-badge) 5 | ![Main Workflow Status](https://img.shields.io/github/actions/workflow/status/yuschick/stylelint-plugin-logical-css/main.yaml?style=for-the-badge) 6 | 7 | Stylelint Plugin Logical CSS aims to enforce the use of logical CSS properties, 8 | values and units. The plugin provides two rules. But first, let's get set up. 9 | 10 | > [Read more about Logical CSS on MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Logical_Properties) 11 | 12 | ## Getting Started 13 | 14 | > Before getting started with the plugin, you must first have 15 | > [Stylelint](https://stylelint.io/) version 14.0.0 or greater installed 16 | 17 | To get started using the plugin, it must first be installed. 18 | 19 | ```bash 20 | npm i stylelint-plugin-logical-css --save-dev 21 | ``` 22 | 23 | ```bash 24 | yarn add stylelint-plugin-logical-css --dev 25 | ``` 26 | 27 | With the plugin installed, the rule(s) can be added to the project's Stylelint 28 | configuration. 29 | 30 | ```json 31 | { 32 | "plugins": ["stylelint-plugin-logical-css"], 33 | "rules": { 34 | "plugin/use-logical-properties-and-values": [ 35 | true, 36 | { "severity": "warning" } 37 | ], 38 | "plugin/use-logical-units": [true, { "severity": "warning" }] 39 | } 40 | } 41 | ``` 42 | 43 | ## Rules 44 | 45 | - [plugin/use-logical-properties-and-values](#pluginuse-logical-properties-and-values) 46 | - [plugin/use-logical-units](#pluginuse-logical-units) 47 | 48 | --- 49 | 50 | ### plugin/use-logical-properties-and-values 51 | 52 | This rule is responsible for checking both CSS properties and values. When a 53 | physical property or value is found, it will be flagged. 54 | 55 | ```json 56 | { 57 | "rules": { 58 | "plugin/use-logical-properties-and-values": [ 59 | true, 60 | { "severity": "warning" } 61 | ] 62 | } 63 | } 64 | ``` 65 | 66 | #### Options 67 | 68 | > Note: As of v0.13.0, the original `disable-auto-fix` option has been removed. 69 | > Please use Stylelint's `disableFix` option instead. 70 | 71 | | Option | Description | 72 | | ------ | ------------------------------------------------------------- | 73 | | ignore | Pass an array of physical properties to ignore while linting. | 74 | 75 | ```json 76 | { 77 | "rules": { 78 | "plugin/use-logical-properties-and-values": [ 79 | true, 80 | { 81 | "severity": "warning", 82 | "ignore": ["overflow-y", "overflow-x"] 83 | } 84 | ] 85 | } 86 | } 87 | ``` 88 | 89 | #### Usage 90 | 91 | ##### Not Allowed 92 | 93 | ```css 94 | .heading { 95 | max-width: 90ch; /* Will flag the use of "width" */ 96 | text-align: left; /* Will flag the use of "left" */ 97 | opacity: 1; 98 | transition: 99 | opacity 1s ease, 100 | max-width 1s ease; /* Will flag the use of 'max-width' */ 101 | } 102 | ``` 103 | 104 | ##### Allowed 105 | 106 | ```css 107 | .heading { 108 | max-inline-size: 90ch; 109 | text-align: start; 110 | opacity: 1; 111 | transition: opacity 1s ease, max-inline-size: 1s ease; 112 | } 113 | ``` 114 | 115 | #### Supported Properties and Values 116 | 117 | ##### Properties for sizing 118 | 119 | | Physical Property |  Logical Property | 120 | | ----------------- | ----------------- | 121 | | `height` | `block-size` | 122 | | `width` | `inline-size` | 123 | | `max-height` | `max-block-size` | 124 | | `max-width` | `max-inline-size` | 125 | | `min-height` | `min-block-size` | 126 | | `min-width` | `min-inline-size` | 127 | 128 | ##### Properties for margins, borders, and padding 129 | 130 | | Physical Property |  Logical Property | 131 | | ------------------------------------------ | --------------------------- | 132 | | `border-top` & `border-bottom` | `border-block` | 133 | | `border-top-color` & `border-bottom-color` | `border-block-color` | 134 | | `border-top-style` & `border-bottom-style` | `border-block-style` | 135 | | `border-top-width` & `border-bottom-width` | `border-block-width` | 136 | | `border-left` & `border-right` | `border-inline` | 137 | | `border-left-color` & `border-right-color` | `border-inline-color` | 138 | | `border-left-style` & `border-right-style` | `border-inline-style` | 139 | | `border-left-width` & `border-right-width` | `border-inline-width` | 140 | | `border-bottom` | `border-block-end` | 141 | | `border-bottom-color` | `border-block-end-color` | 142 | | `border-bottom-style` | `border-block-end-style` | 143 | | `border-bottom-width` | `border-block-end-width` | 144 | | `border-top` | `border-block-start` | 145 | | `border-top-color` | `border-block-start-color` | 146 | | `border-top-style` | `border-block-start-style` | 147 | | `border-top-width` | `border-block-start-width` | 148 | | `border-right` | `border-inline-end` | 149 | | `border-right-color` | `border-inline-end-color` | 150 | | `border-right-style` | `border-inline-end-style` | 151 | | `border-right-width` | `border-inline-end-width` | 152 | | `border-left` | `border-inline-start` | 153 | | `border-left-color` | `border-inline-start-color` | 154 | | `border-left-style` | `border-inline-start-style` | 155 | | `border-left-width` | `border-inline-start-width` | 156 | | `border-bottom-left-radius` | `border-end-start-radius` | 157 | | `border-bottom-right-radius` | `border-end-end-radius` | 158 | | `border-top-left-radius` | `border-start-start-radius` | 159 | | `border-top-right-radius` | `border-start-end-radius` | 160 | | `margin-top` & `margin-bottom` | `margin-block` | 161 | | `margin-top` | `margin-block-start` | 162 | | `margin-bottom` | `margin-block-end` | 163 | | `margin-left` & `margin-right` | `margin-inline` | 164 | | `margin-left` | `margin-inline-start` | 165 | | `margin-right` | `margin-inline-end` | 166 | | `padding-top` & `padding-bottom` | `padding-block` | 167 | | `padding-top` | `padding-block-start` | 168 | | `padding-bottom` | `padding-block-end` | 169 | | `padding-left` & `padding-right` | `padding-inline` | 170 | | `padding-left` | `padding-inline-start` | 171 | | `padding-right` | `padding-inline-end` | 172 | 173 | ##### Properties for floating and positioning 174 | 175 | | Physical Property |  Logical Property | 176 | | ----------------- | --------------------- | 177 | | `clear: left` | `clear: inline-start` | 178 | | `clear: right` | `clear: inline-end` | 179 | | `float: left` | `float: inline-start` | 180 | | `float: right` | `float: inline-end` | 181 | | `top` & `bottom` | `inset-block` | 182 | | `top` | `inset-block-start` | 183 | | `bottom` | `inset-block-end` | 184 | | `left` & `right` | `inset-inline` | 185 | | `left` | `inset-inline-start` | 186 | | `right` | `inset-inline-end` | 187 | 188 | ##### Properties for size containment 189 | 190 | | Physical Property |  Logical Property | 191 | | -------------------------- | ------------------------------- | 192 | | `contain-intrinsic-height` | `contain-intrinsic-block-size` | 193 | | `contain-intrinsic-width` | `contain-intrinsic-inline-size` | 194 | 195 | ##### Other properties 196 | 197 | | Physical Property |  Logical Property | 198 | | ---------------------------------------------- | ----------------------------------- | 199 | | `(-webkit-)box-orient: vertical` | `(-webkit-)box-orient: block-axis` | 200 | | `(-webkit-)box-orient: horizontal` | `(-webkit-)box-orient: inline-axis` | 201 | | `caption-side: right` | `caption-side: inline-end` | 202 | | `caption-side: left` | `caption-side: inline-start` | 203 | | `overflow-y` | `overflow-block` | 204 | | `overflow-x` | `overflow-inline` | 205 | | `overscroll-behavior-x` | `overscroll-behavior-inline` | 206 | | `overscroll-behavior-y` | `overscroll-behavior-block` | 207 | | `resize: horizontal` | `resize: inline` | 208 | | `resize: vertical` | `resize: block` | 209 | | `scroll-margin-bottom` | `scroll-margin-block-end` | 210 | | `scroll-margin-bottom` & `scroll-margin-top` | `scroll-margin-block` | 211 | | `scroll-margin-left` | `scroll-margin-inline-start` | 212 | | `scroll-margin-left` & `scroll-margin-right` | `scroll-margin-inline` | 213 | | `scroll-margin-right` | `scroll-margin-inline-end` | 214 | | `scroll-margin-top` | `scroll-margin-block-start` | 215 | | `scroll-padding-bottom` | `scroll-padding-block-end` | 216 | | `scroll-padding-bottom` & `scroll-padding-top` | `scroll-padding-block` | 217 | | `scroll-padding-left` | `scroll-padding-inline-start` | 218 | | `scroll-padding-left` & `scroll-padding-right` | `scroll-padding-inline` | 219 | | `scroll-padding-right` | `scroll-padding-inline-end` | 220 | | `scroll-padding-top` | `scroll-padding-block-start` | 221 | | `text-align: left` | `text-align: start` | 222 | | `text-align: right` | `text-align: end` | 223 | 224 | --- 225 | 226 | ### plugin/use-logical-units 227 | 228 | This rule is responsible for checking that logical CSS units are used. 229 | Specifically, viewport units like `vw` and `vh` which stand for viewport width 230 | and viewport height respectively will not reflect different writing modes and 231 | directions. Instead, this rule will enforce the logical equivalents, `vi` and 232 | `vb`. 233 | 234 | ```json 235 | { 236 | "rules": { 237 | "plugin/use-logical-units": [true, { "severity": "warning" }] 238 | } 239 | } 240 | ``` 241 | 242 | #### Options 243 | 244 | > Note: As of v0.13.0, the original `disable-auto-fix` option has been removed. 245 | > Please use Stylelint's `disableFix` option instead. 246 | 247 | | Option | Description | 248 | | ------ | -------------------------------------------------------- | 249 | | ignore | Pass an array of physical units to ignore while linting. | 250 | 251 | ```json 252 | { 253 | "rules": { 254 | "plugin/use-logical-units": [ 255 | true, 256 | { 257 | "severity": "warning", 258 | "ignore": ["dvh", "dvw"] 259 | } 260 | ] 261 | } 262 | } 263 | ``` 264 | 265 | #### Usage 266 | 267 | ##### Not Allowed 268 | 269 | ```css 270 | body { 271 | max-block-size: 100vh; /* Will flag the physical use of viewport "height" */ 272 | } 273 | 274 | .container { 275 | inline-size: clamp( 276 | 10vw, 277 | 100%, 278 | 50vw 279 | ); /* Will flag the physical use of viewport "width" */ 280 | } 281 | ``` 282 | 283 | ##### Allowed 284 | 285 | ```css 286 | body { 287 | max-block-size: 100vb; 288 | } 289 | ``` 290 | 291 | #### Supported Units 292 | 293 | > Read about current 294 | > [browser support for logical viewport units](https://caniuse.com/mdn-css_types_length_viewport_percentage_units_dynamic). 295 | 296 | | Physical Unit |  Logical Unit | 297 | | ------------- | ------------- | 298 | | cqh | cqb | 299 | | cqw | cqi | 300 | | dvh | dvb | 301 | | dvw | dvi | 302 | | lvh | lvb | 303 | | lvw | lvi | 304 | | svh | svb | 305 | | svw | svi | 306 | | vh | vb | 307 | | vw | vi | 308 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | export default { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | clearMocks: true, 3 | preset: 'jest-preset-stylelint', 4 | roots: ['src'], 5 | runner: 'jest-light-runner', 6 | setupFiles: ['./jest.setup.js'], 7 | testEnvironment: 'node', 8 | }; 9 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import { getTestRule } from 'jest-preset-stylelint'; 2 | 3 | global.testRule = getTestRule({ 4 | plugins: ['./src'], 5 | }); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stylelint-plugin-logical-css", 3 | "version": "1.2.3", 4 | "description": "A Stylelint plugin to enforce the use of logical CSS properties, values and units.", 5 | "main": "src/index.js", 6 | "type": "module", 7 | "exports": "./src/index.js", 8 | "files": [ 9 | "src/**/*.js", 10 | "!**/**/*.test.js" 11 | ], 12 | "scripts": { 13 | "jest": "cross-env NODE_OPTIONS=\"--experimental-vm-modules\" jest --runInBand", 14 | "test": "npm run jest", 15 | "test:watch": "npm run jest -- --watch" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/yuschick/stylelint-plugin-logical-css.git" 20 | }, 21 | "author": "Daniel Yuschick", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/yuschick/stylelint-plugin-logical-css/issues" 25 | }, 26 | "homepage": "https://github.com/yuschick/stylelint-plugin-logical-css#readme", 27 | "engines": { 28 | "node": ">=18.12.0" 29 | }, 30 | "keywords": [ 31 | "css", 32 | "csslint", 33 | "internationalization", 34 | "i18n", 35 | "lint", 36 | "linter", 37 | "stylelint", 38 | "stylelint plugin", 39 | "logical css" 40 | ], 41 | "peerDependencies": { 42 | "stylelint": "^14.0.0 || ^15.0.0 || ^16.0.0" 43 | }, 44 | "devDependencies": { 45 | "cross-env": "^7.0.3", 46 | "eslint": "^8.35.0", 47 | "jest": "^29.4.3", 48 | "jest-cli": "^29.4.3", 49 | "jest-light-runner": "^0.6.0", 50 | "jest-preset-stylelint": "^7.0.0", 51 | "lint-staged": "^15.0.2", 52 | "prettier": "^3.0.3", 53 | "prettier-eslint": "^16.1.2", 54 | "stylelint": "^16.1.0" 55 | } 56 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import useLogicalPropertiesAndValues from './rules/use-logical-properties-and-values/index.js'; 2 | import useLogicalUnits from './rules/use-logical-units/index.js'; 3 | 4 | export default [useLogicalPropertiesAndValues, useLogicalUnits]; 5 | -------------------------------------------------------------------------------- /src/rules/use-logical-properties-and-values/base.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | 3 | export const ruleName = 'plugin/use-logical-properties-and-values'; 4 | 5 | export const ruleMessages = stylelint.utils.ruleMessages(ruleName, { 6 | unexpectedProp(physicalProperty, logicalProperty) { 7 | return `Unexpected "${physicalProperty}" property. Use "${logicalProperty}".`; 8 | }, 9 | unexpectedValue(property, physicalValue, logicalValue) { 10 | return `Unexpected "${physicalValue}" value in "${property}" property. Use "${logicalValue}".`; 11 | }, 12 | unexpectedTransitionValue(physicalValue, logicalValue) { 13 | return `Unexpected "${physicalValue}" value in "transition" property. Use "${logicalValue}".`; 14 | }, 15 | }); 16 | 17 | export const ruleMeta = { 18 | url: 'https://github.com/yuschick/stylelint-plugin-logical-css', 19 | }; 20 | -------------------------------------------------------------------------------- /src/rules/use-logical-properties-and-values/index.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | 3 | import { ruleName, ruleMessages, ruleMeta } from './base.js'; 4 | import { vendorPrefixes } from '../../utils/vendorPrefixes.js'; 5 | import { physicalProperties } from '../../utils/physical.js'; 6 | import { isPhysicalProperty } from '../../utils/isPhysicalProperty.js'; 7 | import { isPhysicalValue } from '../../utils/isPhysicalValue.js'; 8 | import { physicalPropertiesMap } from '../../utils/physicalPropertiesMap.js'; 9 | import { physicalValuesMap } from '../../utils/physicalValuesMap.js'; 10 | 11 | const ruleFunction = (_, options, context) => { 12 | return (root, result) => { 13 | const validOptions = stylelint.utils.validateOptions(result, ruleName); 14 | 15 | if (!validOptions) { 16 | return; 17 | } 18 | 19 | root.walkDecls((decl) => { 20 | let rootProp = decl.prop; 21 | if ( 22 | Array.isArray(options?.ignore) && 23 | options?.ignore.includes(rootProp) 24 | ) { 25 | return; 26 | } 27 | 28 | vendorPrefixes.forEach( 29 | (prefix) => (rootProp = rootProp.replace(prefix, '')), 30 | ); 31 | 32 | const isValidProp = [ 33 | ...Object.values(physicalProperties), 34 | 'transition', 35 | ].includes(rootProp); 36 | if (!isValidProp) return; 37 | 38 | const isCaptionSideProperty = rootProp === 'caption-side'; 39 | const isBlockAxisCaptionSideValue = 40 | isCaptionSideProperty && ['top', 'bottom'].includes(decl.value); 41 | 42 | const isTransitionProperty = rootProp === 'transition'; 43 | const physicalTransitionProperties = 44 | isTransitionProperty && 45 | Object.values(physicalProperties) 46 | .filter((property) => !(options?.ignore || []).includes(property)) 47 | .flatMap((property) => { 48 | const exp = new RegExp(`(^|[^\\w-])${property}([^\\w-]|$)`); 49 | return decl.value.match(exp); 50 | }) 51 | .filter((p) => p && p.trim()); 52 | 53 | const propIsPhysical = isPhysicalProperty(decl.prop); 54 | const valueIsPhysical = isPhysicalValue(decl.value); 55 | 56 | if ( 57 | (!propIsPhysical && 58 | !valueIsPhysical && 59 | !physicalTransitionProperties.length) || 60 | isBlockAxisCaptionSideValue 61 | ) { 62 | return; 63 | } 64 | 65 | let message; 66 | 67 | if (propIsPhysical) { 68 | message = ruleMessages.unexpectedProp( 69 | decl.prop, 70 | physicalPropertiesMap[rootProp], 71 | ); 72 | } 73 | 74 | if (valueIsPhysical) { 75 | message = ruleMessages.unexpectedValue( 76 | decl.prop, 77 | decl.value, 78 | physicalValuesMap[rootProp][decl.value], 79 | ); 80 | } 81 | 82 | if (physicalTransitionProperties.length) { 83 | const propertyToFlag = physicalTransitionProperties[0].trim(); 84 | message = ruleMessages.unexpectedTransitionValue( 85 | propertyToFlag, 86 | physicalPropertiesMap[propertyToFlag], 87 | ); 88 | } 89 | 90 | if (context.fix) { 91 | if (propIsPhysical) { 92 | decl.prop = physicalPropertiesMap[rootProp]; 93 | } 94 | 95 | if (valueIsPhysical) { 96 | decl.value = physicalValuesMap[rootProp][decl.value]; 97 | } 98 | 99 | if (physicalTransitionProperties.length) { 100 | let newValue = decl.value; 101 | physicalTransitionProperties.forEach((property) => { 102 | newValue = newValue.replace( 103 | property.trim(), 104 | physicalPropertiesMap[property.trim()], 105 | ); 106 | }); 107 | 108 | decl.value = newValue; 109 | } 110 | 111 | return; 112 | } 113 | 114 | stylelint.utils.report({ 115 | message, 116 | node: decl, 117 | result, 118 | ruleName, 119 | }); 120 | }); 121 | }; 122 | }; 123 | 124 | ruleFunction.ruleName = ruleName; 125 | ruleFunction.messages = ruleMessages; 126 | ruleFunction.meta = ruleMeta; 127 | 128 | export default stylelint.createPlugin(ruleName, ruleFunction); 129 | -------------------------------------------------------------------------------- /src/rules/use-logical-properties-and-values/index.test.js: -------------------------------------------------------------------------------- 1 | import rule from './index.js'; 2 | import { logicalProperties } from '../../utils/logical.js'; 3 | import { physicalPropertiesMap } from '../../utils/physicalPropertiesMap.js'; 4 | 5 | const { messages, ruleName } = rule.rule; 6 | 7 | /* eslint-disable-next-line no-undef */ 8 | testRule({ 9 | ruleName, 10 | config: [true], 11 | plugins: ['./index.js'], 12 | fix: true, 13 | accept: [ 14 | // PROPERTIES 15 | ...Object.values(logicalProperties).map((property) => ({ 16 | code: `h1 { ${property}: 1rem; };`, 17 | description: `Using the ${property} property`, 18 | })), 19 | 20 | { 21 | code: 'div { -webkit-box-orient: block-axis; };', 22 | description: 'Testing to -webkit-box-orient property', 23 | }, 24 | { 25 | code: 'div { box-orient: block-axis; };', 26 | description: 'Testing to -webkit-box-orient property', 27 | }, 28 | { 29 | code: 'div { transition: inline-size 1s ease; };', 30 | description: 'Testing a transition property with logical property value.', 31 | }, 32 | { 33 | code: 'div { transition: inline-size 1s ease, block-size 1s ease; };', 34 | description: 'Testing a transition property with logical property value.', 35 | }, 36 | { 37 | code: 'div { transition: inline-size var(--width-duration) ease block-size var(--height-duration) ease; };', 38 | description: 'Testing a transition property with logical property value.', 39 | }, 40 | 41 | // PROPERTIES TO SKIP 42 | { 43 | code: 'div { background: url() top left no-repeat; };', 44 | description: 'Testing to background property to skip', 45 | }, 46 | { 47 | code: 'div { background-position: bottom; };', 48 | description: 'Testing to background-position property to skip', 49 | }, 50 | { 51 | code: 'div { background-position-x: right; };', 52 | description: 'Testing to background-position-x property to skip', 53 | }, 54 | { 55 | code: 'div { background-position-y: bottom; };', 56 | description: 'Testing to background-position-y property to skip', 57 | }, 58 | { 59 | code: 'div { grid-area: bottom; };', 60 | description: 'Testing to grid-area property to skip', 61 | }, 62 | { 63 | code: 'div { grid-template-areas: left right; };', 64 | description: 'Testing to grid-template-areas property to skip', 65 | }, 66 | { 67 | code: 'div { -webkit-mask-position: top right; };', 68 | description: 'Testing to -webkit-mask-position property to skip', 69 | }, 70 | { 71 | code: 'div { mask-position: top right; };', 72 | description: 'Testing to mask-position property to skip', 73 | }, 74 | { 75 | code: 'button { transform-origin: left; };', 76 | description: 'Testing to transform-origin property to skip', 77 | }, 78 | { 79 | code: 'button { vertical-align: bottom; };', 80 | description: 'Testing to vertical-align property to skip', 81 | }, 82 | 83 | // VALUES 84 | { 85 | code: 'table { caption-side: bottom; };', 86 | description: 'Testing block-xx caption side property to skip', 87 | }, 88 | { 89 | code: 'table { caption-side: top; };', 90 | description: 'Testing block-xx caption side property to skip', 91 | }, 92 | { 93 | code: 'table { caption-side: inline-start; };', 94 | description: 'Using a logical caption-side value', 95 | }, 96 | { 97 | code: 'table { caption-side: inline-end; };', 98 | description: 'Using a logical caption-side value', 99 | }, 100 | { 101 | code: 'div { clear: inline-start; };', 102 | description: 'Using a logical clear value', 103 | }, 104 | { 105 | code: 'div { clear: inline-end; };', 106 | description: 'Using a logical clear value', 107 | }, 108 | { 109 | code: 'div { float: inline-start; };', 110 | description: 'Using a logical clear value', 111 | }, 112 | { 113 | code: 'div { float: inline-end; };', 114 | description: 'Using a logical clear value', 115 | }, 116 | { 117 | code: 'div { resize: block; };', 118 | description: 'Using a logical resize value', 119 | }, 120 | { 121 | code: 'div { resize: inline; };', 122 | description: 'Using a logical resize value', 123 | }, 124 | { 125 | code: 'h1 { text-align: start; };', 126 | description: 'Using a logical text-align value', 127 | }, 128 | { 129 | code: 'h1 { text-align: end; };', 130 | description: 'Using a logical text-align value', 131 | }, 132 | ], 133 | 134 | reject: [ 135 | // PROPERTIES 136 | ...Object.keys(physicalPropertiesMap).map((property) => ({ 137 | code: `body { ${property}: 1rem; };`, 138 | description: `Using the physical ${property} property`, 139 | message: messages.unexpectedProp( 140 | property, 141 | physicalPropertiesMap[property], 142 | ), 143 | fixed: `body { ${physicalPropertiesMap[property]}: 1rem; };`, 144 | })), 145 | 146 | // VALUES 147 | { 148 | code: 'div { transition: width 1s ease; };', 149 | description: 'Using a transition property with physical property value.', 150 | message: messages.unexpectedTransitionValue('width', 'inline-size'), 151 | fixed: 'div { transition: inline-size 1s ease; };', 152 | }, 153 | { 154 | code: 'div { transition: width 1s ease, opacity 1s ease; };', 155 | description: 156 | 'Using a transition property with physical property value and unrelated value.', 157 | message: messages.unexpectedTransitionValue('width', 'inline-size'), 158 | fixed: 'div { transition: inline-size 1s ease, opacity 1s ease; };', 159 | }, 160 | { 161 | code: 'div { transition: width 1s ease, height 1s ease; };', 162 | description: 163 | 'Using a transition property with multiple physical property values.', 164 | message: messages.unexpectedTransitionValue('height', 'block-size'), 165 | fixed: 'div { transition: inline-size 1s ease, block-size 1s ease; };', 166 | }, 167 | { 168 | code: 'div { transition: border-top 1s ease, opacity 1s var(--test-width-var); };', 169 | description: 170 | 'Using a transition property with multiple physical property values.', 171 | message: messages.unexpectedTransitionValue( 172 | 'border-top', 173 | 'border-block-start', 174 | ), 175 | fixed: 176 | 'div { transition: border-block-start 1s ease, opacity 1s var(--test-width-var); };', 177 | }, 178 | { 179 | code: 'p { -webkit-box-orient: vertical; };', 180 | description: 'Using a physical -webkit-box-orient value', 181 | message: messages.unexpectedValue( 182 | '-webkit-box-orient', 183 | 'vertical', 184 | 'block-axis', 185 | ), 186 | fixed: 'p { -webkit-box-orient: block-axis; };', 187 | }, 188 | { 189 | code: 'p { -webkit-box-orient: horizontal; };', 190 | description: 'Using a physical -webkit-box-orient value', 191 | message: messages.unexpectedValue( 192 | '-webkit-box-orient', 193 | 'horizontal', 194 | 'inline-axis', 195 | ), 196 | fixed: 'p { -webkit-box-orient: inline-axis; };', 197 | }, 198 | { 199 | code: 'p { -moz-box-orient: horizontal; };', 200 | description: 'Using a physical -moz-box-orient value', 201 | message: messages.unexpectedValue( 202 | '-moz-box-orient', 203 | 'horizontal', 204 | 'inline-axis', 205 | ), 206 | fixed: 'p { -moz-box-orient: inline-axis; };', 207 | }, 208 | { 209 | code: 'p { box-orient: vertical; };', 210 | description: 'Using a physical box-orient value', 211 | message: messages.unexpectedValue('box-orient', 'vertical', 'block-axis'), 212 | fixed: 'p { box-orient: block-axis; };', 213 | }, 214 | { 215 | code: 'p { box-orient: horizontal; };', 216 | description: 'Using a physical box-orient value', 217 | message: messages.unexpectedValue( 218 | 'box-orient', 219 | 'horizontal', 220 | 'inline-axis', 221 | ), 222 | fixed: 'p { box-orient: inline-axis; };', 223 | }, 224 | { 225 | code: 'table { caption-side: left; };', 226 | description: 'Using a physical caption-side value', 227 | message: messages.unexpectedValue('caption-side', 'left', 'inline-start'), 228 | fixed: 'table { caption-side: inline-start; };', 229 | }, 230 | { 231 | code: 'table { caption-side: right; };', 232 | description: 'Using a physical caption-side value', 233 | message: messages.unexpectedValue('caption-side', 'right', 'inline-end'), 234 | fixed: 'table { caption-side: inline-end; };', 235 | }, 236 | { 237 | code: 'div { clear: left; };', 238 | description: 'Using a physical clear value', 239 | message: messages.unexpectedValue('clear', 'left', 'inline-start'), 240 | fixed: 'div { clear: inline-start; };', 241 | }, 242 | { 243 | code: 'div { clear: right; };', 244 | description: 'Using a physical clear value', 245 | message: messages.unexpectedValue('clear', 'right', 'inline-end'), 246 | fixed: 'div { clear: inline-end; };', 247 | }, 248 | { 249 | code: 'div { float: left; };', 250 | description: 'Using a physical float value', 251 | message: messages.unexpectedValue('float', 'left', 'inline-start'), 252 | fixed: 'div { float: inline-start; };', 253 | }, 254 | { 255 | code: 'div { float: right; };', 256 | description: 'Using a physical float value', 257 | message: messages.unexpectedValue('float', 'right', 'inline-end'), 258 | fixed: 'div { float: inline-end; };', 259 | }, 260 | { 261 | code: 'div { resize: horizontal; };', 262 | description: 'Using a physical resize value', 263 | message: messages.unexpectedValue('resize', 'horizontal', 'inline'), 264 | fixed: 'div { resize: inline; };', 265 | }, 266 | { 267 | code: 'div { resize: vertical; };', 268 | description: 'Using a physical resize value', 269 | message: messages.unexpectedValue('resize', 'vertical', 'block'), 270 | fixed: 'div { resize: block; };', 271 | }, 272 | 273 | { 274 | code: 'h1 { text-align: left; };', 275 | description: 'Using a physical text-align value', 276 | message: messages.unexpectedValue('text-align', 'left', 'start'), 277 | fixed: 'h1 { text-align: start; };', 278 | }, 279 | { 280 | code: 'h1 { text-align: right; };', 281 | description: 'Using a physical text-align value', 282 | message: messages.unexpectedValue('text-align', 'right', 'end'), 283 | fixed: 'h1 { text-align: end; };', 284 | }, 285 | ], 286 | }); 287 | 288 | /* eslint-disable-next-line no-undef */ 289 | testRule({ 290 | ruleName, 291 | config: [true, { ignore: ['height', 'overflow-y'] }], 292 | plugins: ['./index.js'], 293 | accept: [ 294 | { 295 | code: `div { overflow-y: auto; };`, 296 | description: 'Allow overflow-y when the option is disabled with false.', 297 | }, 298 | { 299 | code: `div { transition: height 1s ease-in-out; };`, 300 | description: 'Allow height within transition when the property is ignored', 301 | } 302 | ], 303 | reject: [ 304 | { 305 | code: `div { transition: width 1s ease-in-out; };`, 306 | description: 'Using a physical property within transition that is not ignored', 307 | message: messages.unexpectedTransitionValue("width", "inline-size"), 308 | fixed: `div { transition: inline-size 1s ease-in-out; };`, 309 | }, 310 | ], 311 | }); 312 | -------------------------------------------------------------------------------- /src/rules/use-logical-units/base.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | 3 | export const ruleName = 'plugin/use-logical-units'; 4 | 5 | export const ruleMessages = stylelint.utils.ruleMessages(ruleName, { 6 | unexpectedUnit(physicalUnit, logicalUnit) { 7 | return `Unexpected "${physicalUnit}" unit. Use "${logicalUnit}".`; 8 | }, 9 | }); 10 | 11 | export const ruleMeta = { 12 | url: 'https://github.com/yuschick/stylelint-plugin-logical-css', 13 | }; 14 | -------------------------------------------------------------------------------- /src/rules/use-logical-units/index.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | 3 | import { ruleName, ruleMessages, ruleMeta } from './base.js'; 4 | import { getValueUnit, isPhysicalUnit } from '../../utils/isPhysicalUnit.js'; 5 | import { physicalUnitsMap } from '../../utils/physicalUnitsMap.js'; 6 | 7 | const ruleFunction = (_, options, context) => { 8 | return (root, result) => { 9 | const validOptions = stylelint.utils.validateOptions(result, ruleName); 10 | 11 | if (!validOptions) { 12 | return; 13 | } 14 | 15 | root.walkDecls((decl) => { 16 | const unitIsPhysical = isPhysicalUnit(decl.value); 17 | 18 | if (!unitIsPhysical) return; 19 | 20 | const physicalUnit = getValueUnit(decl.value); 21 | 22 | const ignore = physicalUnit.some( 23 | (unit) => 24 | Array.isArray(options?.ignore) && options?.ignore.includes(unit), 25 | ); 26 | 27 | if (ignore) { 28 | return; 29 | } 30 | 31 | const message = ruleMessages.unexpectedUnit( 32 | physicalUnit, 33 | physicalUnit.map((unit) => physicalUnitsMap[unit]), 34 | ); 35 | 36 | if (context.fix) { 37 | physicalUnit.forEach( 38 | (unit) => 39 | (decl.value = decl.value.replace(unit, physicalUnitsMap[unit])), 40 | ); 41 | 42 | return; 43 | } 44 | 45 | stylelint.utils.report({ 46 | message, 47 | node: decl, 48 | result, 49 | ruleName, 50 | }); 51 | }); 52 | }; 53 | }; 54 | 55 | ruleFunction.ruleName = ruleName; 56 | ruleFunction.messages = ruleMessages; 57 | ruleFunction.meta = ruleMeta; 58 | 59 | export default stylelint.createPlugin(ruleName, ruleFunction); 60 | -------------------------------------------------------------------------------- /src/rules/use-logical-units/index.test.js: -------------------------------------------------------------------------------- 1 | import rule from './index.js'; 2 | import { logicalUnits } from '../../utils/logical.js'; 3 | import { physicalUnitsMap } from '../../utils/physicalUnitsMap.js'; 4 | 5 | const { messages, ruleName } = rule.rule; 6 | 7 | /* eslint-disable-next-line no-undef */ 8 | testRule({ 9 | ruleName, 10 | config: [true], 11 | plugins: ['./index.js'], 12 | fix: true, 13 | accept: [ 14 | ...Object.values(logicalUnits).map((unit) => ({ 15 | code: `h1 { block-size: 100${unit}; };`, 16 | description: `Using the logical ${unit} unit`, 17 | })), 18 | { 19 | code: 'div { inline-size: min(80vi, 100%); };', 20 | description: 'Testing physical unit inside a function', 21 | }, 22 | { 23 | code: 'div { inline-size: min(100%, 80vi) };', 24 | description: 'Testing physical unit inside a function', 25 | }, 26 | ], 27 | 28 | reject: [ 29 | ...Object.keys(physicalUnitsMap).map((unit) => ({ 30 | code: `body { block-size: 100${unit}; };`, 31 | description: `Using the physical ${unit} unit`, 32 | message: messages.unexpectedUnit(unit, physicalUnitsMap[unit]), 33 | fixed: `body { block-size: 100${physicalUnitsMap[unit]}; };`, 34 | })), 35 | { 36 | code: 'div { inline-size: min(80vw, 100%) };', 37 | description: 'Testing physical unit inside a function', 38 | message: messages.unexpectedUnit('vw', physicalUnitsMap.vw), 39 | fixed: `div { inline-size: min(80vi, 100%) };`, 40 | }, 41 | { 42 | code: 'div { inline-size: 80vh; };', 43 | description: 'Testing physical unit inside a clamp function', 44 | message: messages.unexpectedUnit('vh', physicalUnitsMap.vh), 45 | fixed: `div { inline-size: 80vb; };`, 46 | }, 47 | { 48 | code: 'div { inline-size: clamp(80vh, 50%, 90vw); };', 49 | description: 'Testing physical unit inside a clamp function', 50 | message: messages.unexpectedUnit( 51 | 'vh,vw', 52 | `${physicalUnitsMap.vh},${physicalUnitsMap.vw}`, 53 | ), 54 | fixed: `div { inline-size: clamp(80vb, 50%, 90vi); };`, 55 | }, 56 | { 57 | code: 'div { inline-size: 50.5vw; };', 58 | description: 'Testing float physical unit', 59 | message: messages.unexpectedUnit('vw', physicalUnitsMap.vw), 60 | fixed: `div { inline-size: 50.5vi; };`, 61 | }, 62 | ], 63 | }); 64 | 65 | /* eslint-disable-next-line no-undef */ 66 | testRule({ 67 | ruleName, 68 | config: [true, { ignore: ['dvh'] }], 69 | plugins: ['./index.js'], 70 | accept: [ 71 | { 72 | code: 'div { top: 1dvh; };', 73 | description: 'Allow dvh unit when the option is disabled.', 74 | }, 75 | ], 76 | reject: [ 77 | { 78 | code: 'div { top: 1dvw; };', 79 | description: 'Using a physical unit that is not disabled in the options.', 80 | message: messages.unexpectedUnit('dvw', 'dvi'), 81 | }, 82 | ], 83 | }); 84 | -------------------------------------------------------------------------------- /src/utils/isPhysicalProperty.js: -------------------------------------------------------------------------------- 1 | import { physicalPropertiesMap } from './physicalPropertiesMap.js'; 2 | 3 | export function isPhysicalProperty(property) { 4 | const physicalProperties = Object.keys(physicalPropertiesMap); 5 | 6 | const propIsPhysical = physicalProperties.includes(property); 7 | 8 | return propIsPhysical; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/isPhysicalUnit.js: -------------------------------------------------------------------------------- 1 | import { physicalUnits } from './physical.js'; 2 | 3 | const expression = /\b\d+(\.\d+)?\s*(cqh|cqw|dvh|dvw|lvh|lvw|svh|svw|vh|vw)\b/g; 4 | 5 | export function getValueUnit(value) { 6 | const match = value.match(expression); 7 | 8 | if (!match) return false; 9 | 10 | const matches = Array.isArray(match) ? match : [match]; 11 | 12 | const matchedUnit = matches.map( 13 | (match) => physicalUnits[match.replace(/[0-9](\.[0-9])?/g, '')], 14 | ); 15 | 16 | return matchedUnit; 17 | } 18 | 19 | export function isPhysicalUnit(value) { 20 | const units = getValueUnit(value); 21 | 22 | if (!units) return false; 23 | 24 | const unitIsPhysical = Object.values(physicalUnits).some((unit) => 25 | units.find((match) => match.includes(unit)), 26 | ); 27 | 28 | return unitIsPhysical; 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/isPhysicalValue.js: -------------------------------------------------------------------------------- 1 | import { physicalAxis, physicalValues } from './physical.js'; 2 | 3 | export function isPhysicalValue(value) { 4 | const values = [ 5 | ...Object.values(physicalValues), 6 | ...Object.values(physicalAxis), 7 | ]; 8 | 9 | const valueIsPhysical = values.some((direction) => value === direction); 10 | 11 | return valueIsPhysical; 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/logical.js: -------------------------------------------------------------------------------- 1 | export const logicalAxis = Object.freeze({ 2 | block: 'block', 3 | inline: 'inline', 4 | }); 5 | 6 | export const logicalInlinePoints = Object.freeze({ 7 | end: 'end', 8 | start: 'start', 9 | }); 10 | 11 | export const logicalProperties = Object.freeze({ 12 | blockSize: 'block-size', 13 | borderBlock: 'border-block', 14 | borderBlockColor: 'border-block-color', 15 | borderBlockEnd: 'border-block-end', 16 | borderBlockEndColor: 'border-block-end-color', 17 | borderBlockEndStyle: 'border-block-end-style', 18 | borderBlockEndWidth: 'border-block-end-width', 19 | borderBlockStart: 'border-block-start', 20 | borderBlockStartColor: 'border-block-start-color', 21 | borderBlockStartStyle: 'border-block-start-style', 22 | borderBlockStartWidth: 'border-block-start-width', 23 | borderBlockStyle: 'border-block-style', 24 | borderBlockWidth: 'border-block-width', 25 | borderColor: 'border-color', 26 | borderEndEndRadius: 'border-end-end-radius', 27 | borderEndStartRadius: 'border-end-start-radius', 28 | borderInline: 'border-inline', 29 | borderInlineColor: 'border-inline-color', 30 | borderInlineEnd: 'border-inline-end', 31 | borderInlineEndColor: 'border-inline-end-color', 32 | borderInlineEndStyle: 'border-inline-end-style', 33 | borderInlineEndWidth: 'border-inline-end-width', 34 | borderInlineStart: 'border-inline-start', 35 | borderInlineStartColor: 'border-inline-start-color', 36 | borderInlineStartStyle: 'border-inline-start-style', 37 | borderInlineStartWidth: 'border-inline-start-width', 38 | borderInlineStyle: 'border-inline-style', 39 | borderInlineWidth: 'border-inline-width', 40 | borderStartEndRadius: 'border-start-end-radius', 41 | borderStartStartRadius: 'border-start-start-radius', 42 | borderStyle: 'border-style', 43 | borderWidth: 'border-width', 44 | containIntrinsicBlockSize: 'contain-intrinsic-block-size', 45 | containIntrinsicInlineSize: 'contain-intrinsic-inline-size', 46 | inlineSize: 'inline-size', 47 | insetBlock: 'inset-block', 48 | insetBlockEnd: 'inset-block-end', 49 | insetBlockStart: 'inset-block-start', 50 | insetInline: 'inset-inline', 51 | insetInlineEnd: 'inset-inline-end', 52 | insetInlineStart: 'inset-inline-start', 53 | marginBlock: 'margin-block', 54 | marginBlockEnd: 'margin-block-end', 55 | marginBlockStart: 'margin-block-start', 56 | marginInline: 'margin-inline', 57 | marginInlineEnd: 'margin-inline-end', 58 | marginInlineStart: 'margin-inline-start', 59 | maxBlockSize: 'max-block-size', 60 | maxInlineSize: 'max-inline-size', 61 | minBlockSize: 'min-block-size', 62 | minInlineSize: 'min-inline-size', 63 | overflowBlock: 'overflow-block', 64 | overflowInline: 'overflow-inline', 65 | overscrollBehaviorBlock: 'overscroll-behavior-block', 66 | overscrollBehaviorInline: 'overscroll-behavior-inline', 67 | paddingBlock: 'padding-block', 68 | paddingBlockEnd: 'padding-block-end', 69 | paddingBlockStart: 'padding-block-start', 70 | paddingInline: 'padding-inline', 71 | paddingInlineEnd: 'padding-inline-end', 72 | paddingInlineStart: 'padding-inline-start', 73 | scrollMarginBlock: 'scroll-margin-block', 74 | scrollMarginBlockEnd: 'scroll-margin-block-end', 75 | scrollMarginBlockStart: 'scroll-margin-block-start', 76 | scrollMarginInline: 'scroll-margin-inline', 77 | scrollMarginInlineEnd: 'scroll-margin-inline-end', 78 | scrollMarginInlineStart: 'scroll-margin-inline-start', 79 | scrollPaddingBlock: 'scroll-padding-block', 80 | scrollPaddingBlockEnd: 'scroll-padding-block-end', 81 | scrollPaddingBlockStart: 'scroll-padding-block-start', 82 | scrollPaddingInline: 'scroll-padding-inline', 83 | scrollPaddingInlineEnd: 'scroll-padding-inline-end', 84 | scrollPaddingInlineStart: 'scroll-padding-inline-start', 85 | }); 86 | 87 | export const logicalUnits = Object.freeze({ 88 | cqb: 'cqb', 89 | cqi: 'cqi', 90 | dvb: 'dvb', 91 | dvi: 'dvi', 92 | lvb: 'lvb', 93 | lvi: 'lvi', 94 | svb: 'svb', 95 | svi: 'svi', 96 | vb: 'vb', 97 | vi: 'vi', 98 | }); 99 | 100 | export const logicalValues = Object.freeze({ 101 | blockEnd: 'block-end', 102 | blockStart: 'block-start', 103 | inlineEnd: 'inline-end', 104 | inlineStart: 'inline-start', 105 | }); 106 | -------------------------------------------------------------------------------- /src/utils/physical.js: -------------------------------------------------------------------------------- 1 | export const physicalAxis = Object.freeze({ 2 | horizontal: 'horizontal', 3 | vertical: 'vertical', 4 | x: 'x', 5 | y: 'y', 6 | }); 7 | 8 | export const physicalProperties = Object.freeze({ 9 | borderBottom: 'border-bottom', 10 | borderBottomColor: 'border-bottom-color', 11 | borderBottomLeftRadius: 'border-bottom-left-radius', 12 | borderBottomRightRadius: 'border-bottom-right-radius', 13 | borderBottomStyle: 'border-bottom-style', 14 | borderBottomWidth: 'border-bottom-width', 15 | borderLeft: 'border-left', 16 | borderLeftColor: 'border-left-color', 17 | borderLeftStyle: 'border-left-style', 18 | borderLeftWidth: 'border-left-width', 19 | borderRight: 'border-right', 20 | borderRightColor: 'border-right-color', 21 | borderRightStyle: 'border-right-style', 22 | borderRightWidth: 'border-right-width', 23 | borderTop: 'border-top', 24 | borderTopColor: 'border-top-color', 25 | borderTopLeftRadius: 'border-top-left-radius', 26 | borderTopRightRadius: 'border-top-right-radius', 27 | borderTopStyle: 'border-top-style', 28 | borderTopWidth: 'border-top-width', 29 | boxOrient: 'box-orient', 30 | bottom: 'bottom', 31 | captionSide: 'caption-side', 32 | clear: 'clear', 33 | containIntrinsicHeight: 'contain-intrinsic-height', 34 | containIntrinsicWidth: 'contain-intrinsic-width', 35 | float: 'float', 36 | height: 'height', 37 | left: 'left', 38 | marginBottom: 'margin-bottom', 39 | marginLeft: 'margin-left', 40 | marginRight: 'margin-right', 41 | marginTop: 'margin-top', 42 | maxHeight: 'max-height', 43 | maxWidth: 'max-width', 44 | minHeight: 'min-height', 45 | minWidth: 'min-width', 46 | overflowX: 'overflow-x', 47 | overflowY: 'overflow-y', 48 | overscrollBehaviorX: 'overscroll-behavior-x', 49 | overscrollBehaviorY: 'overscroll-behavior-y', 50 | paddingBottom: 'padding-bottom', 51 | paddingLeft: 'padding-left', 52 | paddingRight: 'padding-right', 53 | paddingTop: 'padding-top', 54 | resize: 'resize', 55 | right: 'right', 56 | scrollMarginBottom: 'scroll-margin-bottom', 57 | scrollMarginLeft: 'scroll-margin-left', 58 | scrollMarginRight: 'scroll-margin-right', 59 | scrollMarginTop: 'scroll-margin-top', 60 | scrollPaddingBottom: 'scroll-padding-bottom', 61 | scrollPaddingLeft: 'scroll-padding-left', 62 | scrollPaddingRight: 'scroll-padding-right', 63 | scrollPaddingTop: 'scroll-padding-top', 64 | textAlign: 'text-align', 65 | top: 'top', 66 | width: 'width', 67 | }); 68 | 69 | export const physicalUnits = Object.freeze({ 70 | cqh: 'cqh', 71 | cqw: 'cqw', 72 | dvh: 'dvh', 73 | dvw: 'dvw', 74 | lvh: 'lvh', 75 | lvw: 'lvw', 76 | svh: 'svh', 77 | svw: 'svw', 78 | vh: 'vh', 79 | vw: 'vw', 80 | }); 81 | 82 | export const physicalValues = Object.freeze({ 83 | bottom: 'bottom', 84 | left: 'left', 85 | right: 'right', 86 | top: 'top', 87 | }); 88 | -------------------------------------------------------------------------------- /src/utils/physicalPropertiesMap.js: -------------------------------------------------------------------------------- 1 | import { logicalProperties } from './logical.js'; 2 | import { physicalProperties } from './physical.js'; 3 | 4 | export const physicalPropertiesMap = Object.freeze({ 5 | [physicalProperties.borderBottom]: logicalProperties.borderBlockEnd, 6 | [physicalProperties.borderBottomColor]: logicalProperties.borderBlockEndColor, 7 | [physicalProperties.borderBottomLeftRadius]: 8 | logicalProperties.borderEndStartRadius, 9 | [physicalProperties.borderBottomRightRadius]: 10 | logicalProperties.borderEndEndRadius, 11 | [physicalProperties.borderBottomStyle]: logicalProperties.borderBlockEndStyle, 12 | [physicalProperties.borderBottomWidth]: logicalProperties.borderBlockEndWidth, 13 | [physicalProperties.borderLeft]: logicalProperties.borderInlineStart, 14 | [physicalProperties.borderLeftColor]: 15 | logicalProperties.borderInlineStartColor, 16 | [physicalProperties.borderLeftStyle]: 17 | logicalProperties.borderInlineStartStyle, 18 | [physicalProperties.borderLeftWidth]: 19 | logicalProperties.borderInlineStartWidth, 20 | [physicalProperties.borderRight]: logicalProperties.borderInlineEnd, 21 | [physicalProperties.borderRightColor]: logicalProperties.borderInlineEndColor, 22 | [physicalProperties.borderRightStyle]: logicalProperties.borderInlineEndStyle, 23 | [physicalProperties.borderRightWidth]: logicalProperties.borderInlineEndWidth, 24 | [physicalProperties.borderTop]: logicalProperties.borderBlockStart, 25 | [physicalProperties.borderTopColor]: logicalProperties.borderBlockStartColor, 26 | [physicalProperties.borderTopLeftRadius]: 27 | logicalProperties.borderStartStartRadius, 28 | [physicalProperties.borderTopRightRadius]: 29 | logicalProperties.borderStartEndRadius, 30 | [physicalProperties.borderTopStyle]: logicalProperties.borderBlockStartStyle, 31 | [physicalProperties.borderTopWidth]: logicalProperties.borderBlockStartWidth, 32 | [physicalProperties.bottom]: logicalProperties.insetBlockEnd, 33 | [physicalProperties.containIntrinsicHeight]: 34 | logicalProperties.containIntrinsicBlockSize, 35 | [physicalProperties.containIntrinsicWidth]: 36 | logicalProperties.containIntrinsicInlineSize, 37 | [physicalProperties.height]: logicalProperties.blockSize, 38 | [physicalProperties.left]: logicalProperties.insetInlineStart, 39 | [physicalProperties.marginBottom]: logicalProperties.marginBlockEnd, 40 | [physicalProperties.marginLeft]: logicalProperties.marginInlineStart, 41 | [physicalProperties.marginRight]: logicalProperties.marginInlineEnd, 42 | [physicalProperties.marginTop]: logicalProperties.marginBlockStart, 43 | [physicalProperties.maxHeight]: logicalProperties.maxBlockSize, 44 | [physicalProperties.maxWidth]: logicalProperties.maxInlineSize, 45 | [physicalProperties.minHeight]: logicalProperties.minBlockSize, 46 | [physicalProperties.minWidth]: logicalProperties.minInlineSize, 47 | [physicalProperties.overflowX]: logicalProperties.overflowInline, 48 | [physicalProperties.overflowY]: logicalProperties.overflowBlock, 49 | [physicalProperties.overscrollBehaviorX]: 50 | logicalProperties.overscrollBehaviorInline, 51 | [physicalProperties.overscrollBehaviorY]: 52 | logicalProperties.overscrollBehaviorBlock, 53 | [physicalProperties.paddingBottom]: logicalProperties.paddingBlockEnd, 54 | [physicalProperties.paddingLeft]: logicalProperties.paddingInlineStart, 55 | [physicalProperties.paddingRight]: logicalProperties.paddingInlineEnd, 56 | [physicalProperties.paddingTop]: logicalProperties.paddingBlockStart, 57 | [physicalProperties.scrollMarginBottom]: 58 | logicalProperties.scrollMarginBlockEnd, 59 | [physicalProperties.scrollMarginLeft]: 60 | logicalProperties.scrollMarginInlineStart, 61 | [physicalProperties.scrollMarginRight]: 62 | logicalProperties.scrollMarginInlineEnd, 63 | [physicalProperties.scrollMarginTop]: 64 | logicalProperties.scrollMarginBlockStart, 65 | [physicalProperties.scrollPaddingBottom]: 66 | logicalProperties.scrollPaddingBlockEnd, 67 | [physicalProperties.scrollPaddingLeft]: 68 | logicalProperties.scrollPaddingInlineStart, 69 | [physicalProperties.scrollPaddingRight]: 70 | logicalProperties.scrollPaddingInlineEnd, 71 | [physicalProperties.scrollPaddingTop]: 72 | logicalProperties.scrollPaddingBlockStart, 73 | [physicalProperties.right]: logicalProperties.insetInlineEnd, 74 | [physicalProperties.top]: logicalProperties.insetBlockStart, 75 | [physicalProperties.width]: logicalProperties.inlineSize, 76 | }); 77 | -------------------------------------------------------------------------------- /src/utils/physicalUnitsMap.js: -------------------------------------------------------------------------------- 1 | import { logicalUnits } from './logical.js'; 2 | import { physicalUnits } from './physical.js'; 3 | 4 | export const physicalUnitsMap = Object.freeze({ 5 | [physicalUnits.cqh]: logicalUnits.cqb, 6 | [physicalUnits.cqw]: logicalUnits.cqi, 7 | [physicalUnits.dvh]: logicalUnits.dvb, 8 | [physicalUnits.dvw]: logicalUnits.dvi, 9 | [physicalUnits.lvh]: logicalUnits.lvb, 10 | [physicalUnits.lvw]: logicalUnits.lvi, 11 | [physicalUnits.svh]: logicalUnits.svb, 12 | [physicalUnits.svw]: logicalUnits.svi, 13 | [physicalUnits.vh]: logicalUnits.vb, 14 | [physicalUnits.vw]: logicalUnits.vi, 15 | }); 16 | -------------------------------------------------------------------------------- /src/utils/physicalValuesMap.js: -------------------------------------------------------------------------------- 1 | import { logicalAxis, logicalInlinePoints, logicalValues } from './logical.js'; 2 | import { 3 | physicalAxis, 4 | physicalProperties, 5 | physicalValues, 6 | } from './physical.js'; 7 | 8 | export const physicalValuesMap = Object.freeze({ 9 | [physicalProperties.boxOrient]: { 10 | [physicalAxis.horizontal]: `${logicalAxis.inline}-axis`, 11 | [physicalAxis.vertical]: `${logicalAxis.block}-axis`, 12 | }, 13 | [physicalProperties.captionSide]: { 14 | [physicalValues.left]: logicalValues.inlineStart, 15 | [physicalValues.right]: logicalValues.inlineEnd, 16 | }, 17 | [physicalProperties.clear]: { 18 | [physicalValues.left]: logicalValues.inlineStart, 19 | [physicalValues.right]: logicalValues.inlineEnd, 20 | }, 21 | [physicalProperties.float]: { 22 | [physicalValues.left]: logicalValues.inlineStart, 23 | [physicalValues.right]: logicalValues.inlineEnd, 24 | }, 25 | [physicalProperties.resize]: { 26 | [physicalAxis.horizontal]: logicalAxis.inline, 27 | [physicalAxis.vertical]: logicalAxis.block, 28 | }, 29 | [physicalProperties.textAlign]: { 30 | [physicalValues.left]: logicalInlinePoints.start, 31 | [physicalValues.right]: logicalInlinePoints.end, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/utils/vendorPrefixes.js: -------------------------------------------------------------------------------- 1 | export const vendorPrefixes = ['-webkit-', '-moz-', '-o-', '-ms-']; 2 | --------------------------------------------------------------------------------