stylelint-gamut 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.js ADDED
@@ -0,0 +1,15 @@
1
+ module.exports = {
2
+ env: {
3
+ browser: true,
4
+ commonjs: true,
5
+ es2021: true
6
+ },
7
+ extends: [
8
+ 'standard'
9
+ ],
10
+ parserOptions: {
11
+ ecmaVersion: 'latest'
12
+ },
13
+ rules: {
14
+ }
15
+ }
package/README.md ADDED
@@ -0,0 +1,2 @@
1
+ # stylelint-gamut
2
+ Stylelint plugin for working with different color spaces
package/jest.config.js ADDED
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ preset: 'jest-preset-stylelint',
3
+ clearMocks: true,
4
+ setupFiles: ['./jest.setup.js'],
5
+ testEnvironment: 'node',
6
+ roots: ['src']
7
+ }
package/jest.setup.js ADDED
@@ -0,0 +1,5 @@
1
+ 'use strict'
2
+
3
+ const { getTestRule } = require('jest-preset-stylelint')
4
+
5
+ global.testRule = getTestRule({ plugins: ['./src'] })
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "stylelint-gamut",
3
+ "version": "1.0.0",
4
+ "description": "Stylelint plugin for working with different color spaces",
5
+ "main": "src/index.js",
6
+ "scripts": {
7
+ "test": "jest"
8
+ },
9
+ "engines": {
10
+ "node": ">=14.0.0"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/fpetrakov/stylelint-gamut.git"
15
+ },
16
+ "keywords": [
17
+ "stylelint",
18
+ "stylelint-plugin",
19
+ "gamut",
20
+ "lch",
21
+ "oklch",
22
+ "lab",
23
+ "oklab"
24
+ ],
25
+ "author": "Fedya Petrakov",
26
+ "license": "MIT",
27
+ "bugs": {
28
+ "url": "https://github.com/fpetrakov/stylelint-gamut/issues"
29
+ },
30
+ "homepage": "https://github.com/fpetrakov/stylelint-gamut#readme",
31
+ "peerDependencies": {
32
+ "stylelint": "^14.10.0"
33
+ },
34
+ "devDependencies": {
35
+ "eslint": "^8.22.0",
36
+ "eslint-config-standard": "^17.0.0",
37
+ "eslint-plugin-import": "^2.26.0",
38
+ "eslint-plugin-n": "^15.2.4",
39
+ "eslint-plugin-promise": "^6.0.0",
40
+ "jest": "^28.1.3",
41
+ "jest-cli": "^28.1.3",
42
+ "jest-preset-stylelint": "^5.0.4",
43
+ "stylelint": "^14.10.0"
44
+ },
45
+ "dependencies": {
46
+ "colorjs.io": "^0.4.0"
47
+ }
48
+ }
package/src/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # color-no-out-gamut-range
2
+
3
+ Disallow out of gamut range colors.
4
+
5
+ <!-- prettier-ignore -->
6
+ ```css
7
+ a { color: lch(48% 82 283) }
8
+ /** ↑
9
+ * This color is out of sRGB gamut range */
10
+ ```
11
+
12
+ Colors declared using lch, oklch, lab and oklab functions can move from sRGB to P3 color space, which is not supported by most of screens right now. It can happen by mistake. For instance, by converting colors from one space to another.
13
+
14
+ This rule checks if the color is in sRGB space. If it's not, you should wrap it in media query like this:
15
+
16
+ ```css
17
+ @media (color-gamut: p3) {
18
+ a {
19
+ color: lch(48% 82 283);
20
+ }
21
+ }
22
+ ```
23
+
24
+ Please note that conditional imports are not checked, e.g.:
25
+
26
+ ```html
27
+ <link href="p3-custom-props.css" rel="stylesheet" media="(color-gamut: p3)" />
28
+ ```
29
+
30
+ ```css
31
+ @import url("p3-custom-props.css") (color-gamut: p3);
32
+ ```
33
+
34
+ ## Options
35
+
36
+ ### `true`
37
+
38
+ The following patterns are considered problems:
39
+
40
+ <!-- prettier-ignore -->
41
+ ```css
42
+ a { color: lch(48% 82 283) }
43
+ ```
44
+
45
+ <!-- prettier-ignore -->
46
+ ```css
47
+ a { color: oklch(48% 82 283) }
48
+ ```
49
+
50
+ <!-- prettier-ignore -->
51
+ ```css
52
+ a { color: lab(48% 82 283) }
53
+ ```
54
+
55
+ <!-- prettier-ignore -->
56
+ ```css
57
+ a { color: oklab(48% 82 283) }
58
+ ```
59
+
60
+ The following patterns are _not_ considered problems:
61
+
62
+ <!-- prettier-ignore -->
63
+ ```css
64
+ @media (color-gamut: p3) {
65
+ a { color: lch(48% 82 283) }
66
+ }
67
+ ```
68
+
69
+ <!-- prettier-ignore -->
70
+ ```css
71
+ @media (color-gamut: p3) {
72
+ a { color: oklch(48% 82 283) }
73
+ }
74
+ ```
75
+
76
+ <!-- prettier-ignore -->
77
+ ```css
78
+ @media (color-gamut: p3) {
79
+ a { color: lab(48% 82 283) }
80
+ }
81
+ ```
82
+
83
+ <!-- prettier-ignore -->
84
+ ```css
85
+ @media (color-gamut: p3) {
86
+ a { color: oklab(48% 82 283) }
87
+ }
88
+ ```
package/src/index.js ADDED
@@ -0,0 +1,92 @@
1
+ 'use strict'
2
+
3
+ const valueParser = require('postcss-value-parser')
4
+ const stylelint = require('stylelint')
5
+ const Color = require('colorjs.io').default
6
+ const isStandardSyntaxProperty = require('./utils/isStandardSyntaxProperty')
7
+ const isCustomProperty = require('./utils/isCustomProperty')
8
+ const getDeclarationValue = require('./utils/getDeclarationValue')
9
+ const isAtRule = require('./utils/isAtRule')
10
+ const declarationValueIndex = require('./utils/declarationValueIndex')
11
+
12
+ const ruleName = 'gamut/color-no-out-gamut-range'
13
+
14
+ const messages = stylelint.utils.ruleMessages(ruleName, {
15
+ rejected: (color) => `Unexpected color out of gamut range "${color}"`
16
+ })
17
+
18
+ const meta = {
19
+ url: 'https://stylelint.io/user-guide/rules/list/color-no-out-gamut-range'
20
+ }
21
+
22
+ /** @type {import('stylelint').Rule} */
23
+ module.exports = stylelint.createPlugin(ruleName, (primary) => {
24
+ return (root, result) => {
25
+ const validOptions = stylelint.utils.validateOptions(result, ruleName, {
26
+ actual: primary
27
+ })
28
+
29
+ if (!validOptions) {
30
+ return
31
+ }
32
+
33
+ root.walkDecls((decl) => {
34
+ if (!isStandardSyntaxProperty(decl.prop)) return
35
+
36
+ if (isCustomProperty(decl.prop)) return
37
+
38
+ const parsedValue = valueParser(getDeclarationValue(decl))
39
+
40
+ parsedValue.walk((node, _index, nodes) => {
41
+ if (node.type !== 'function') return
42
+
43
+ if (
44
+ node.value !== 'lch' &&
45
+ node.value !== 'lab' &&
46
+ node.value !== 'oklch' &&
47
+ node.value !== 'oklab'
48
+ ) {
49
+ return
50
+ }
51
+
52
+ const isInSrgbGamut = new Color(valueParser.stringify(nodes)).inGamut(
53
+ 'srgb'
54
+ )
55
+
56
+ if (isInSrgbGamut) return
57
+
58
+ if (isInColorGamutP3MediaQuery(decl)) return
59
+
60
+ const index = declarationValueIndex(decl) + node.sourceIndex
61
+ const endIndex = index + decl.value.length
62
+
63
+ stylelint.utils.report({
64
+ message: messages.rejected(decl.value),
65
+ node: decl,
66
+ index,
67
+ endIndex,
68
+ result,
69
+ ruleName
70
+ })
71
+ })
72
+ })
73
+ }
74
+ })
75
+
76
+ /**
77
+ * @param {import('postcss').Declaration} decl
78
+ * @returns {boolean}
79
+ */
80
+ function isInColorGamutP3MediaQuery (decl) {
81
+ if (decl.parent && decl.parent.parent && isAtRule(decl.parent.parent)) {
82
+ const parent = decl.parent.parent
83
+
84
+ return parent.name === 'media' && parent.params === '(color-gamut: p3)'
85
+ }
86
+
87
+ return false
88
+ }
89
+
90
+ module.exports.ruleName = ruleName
91
+ module.exports.messages = messages
92
+ module.exports.meta = meta
@@ -0,0 +1,89 @@
1
+ 'use strict'
2
+
3
+ const { messages, ruleName } = require('./index.js')
4
+
5
+ testRule({
6
+ ruleName,
7
+ config: [true],
8
+ plugins: ['./index.js'],
9
+ accept: [
10
+ {
11
+ code: '@media (color-gamut: p3) { a { color: lch(48% 82 283); } }',
12
+ description: 'lch out of srgb gamut but wrapped in media query'
13
+ },
14
+ {
15
+ code: '@media (color-gamut: p3) { a { color: oklch(48% 82 283 / 73%); } }',
16
+ description:
17
+ 'oklch with alpha out of srgb gamut but wrapped in media query'
18
+ },
19
+ {
20
+ code: 'a { color: lch(50% 0 0); }',
21
+ description: 'in srgb gamut'
22
+ },
23
+ {
24
+ code: 'a { color: red; }',
25
+ description: 'ignore not lch color declaration'
26
+ },
27
+ {
28
+ code: ':root { --clr: lch(48% 82 283 / 75%) } a { color: var(--clr) }',
29
+ description: 'ignore css variables'
30
+ },
31
+ {
32
+ code: '@myVariable: lch(48% 82 283); a { color: @myVariable; }',
33
+ description: 'ignore Less variable'
34
+ },
35
+ {
36
+ code: ':root { $clr: lch(48% 82 283 / 67%) } a { color: $clr; }',
37
+ description: 'ignore scss variables'
38
+ }
39
+ ],
40
+
41
+ reject: [
42
+ {
43
+ code: 'a { color: lch(48% 82 283); }',
44
+ description: 'lch',
45
+ message: messages.rejected('lch(48% 82 283)'),
46
+ line: 1,
47
+ column: 12,
48
+ endLine: 1,
49
+ endColumn: 27
50
+ },
51
+ {
52
+ code: 'a { color: lch(48% 82 283 / 72%); }',
53
+ description: 'lch with alpha',
54
+ message: messages.rejected('lch(48% 82 283 / 72%)'),
55
+ line: 1,
56
+ column: 12,
57
+ endLine: 1,
58
+ endColumn: 33
59
+ },
60
+ {
61
+ code: 'a { color: oklch(98% 49 129); }',
62
+ description: 'oklch',
63
+ message: messages.rejected('oklch(98% 49 129)'),
64
+ line: 1,
65
+ column: 12,
66
+ endLine: 1,
67
+ endColumn: 29
68
+ },
69
+
70
+ {
71
+ code: 'a { color: lab(98% 49 129); }',
72
+ description: 'lab',
73
+ message: messages.rejected('lab(98% 49 129)'),
74
+ line: 1,
75
+ column: 12,
76
+ endLine: 1,
77
+ endColumn: 27
78
+ },
79
+ {
80
+ code: 'a { color: oklab(98% 49 129); }',
81
+ description: 'oklab',
82
+ message: messages.rejected('oklab(98% 49 129)'),
83
+ line: 1,
84
+ column: 12,
85
+ endLine: 1,
86
+ endColumn: 29
87
+ }
88
+ ]
89
+ })
@@ -0,0 +1,29 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Get the index of a declaration's value
5
+ *
6
+ * @param {import('postcss').Declaration} decl
7
+ * @returns {number}
8
+ */
9
+ module.exports = function declarationValueIndex (decl) {
10
+ const raws = decl.raws
11
+
12
+ return [
13
+ // @ts-expect-error -- TS2571: Object is of type 'unknown'.
14
+ raws.prop && raws.prop.prefix,
15
+ // @ts-expect-error -- TS2571: Object is of type 'unknown'.
16
+ (raws.prop && raws.prop.raw) || decl.prop,
17
+ // @ts-expect-error -- TS2571: Object is of type 'unknown'.
18
+ raws.prop && raws.prop.suffix,
19
+ raws.between || ':',
20
+ // @ts-expect-error -- TS2339: Property 'prefix' does not exist on type '{ value: string; raw: string; }'.
21
+ raws.value && raws.value.prefix
22
+ ].reduce((count, str) => {
23
+ if (str) {
24
+ return count + str.length
25
+ }
26
+
27
+ return count
28
+ }, 0)
29
+ }
@@ -0,0 +1,11 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * @param {import('postcss').Declaration} decl
5
+ * @returns {string}
6
+ */
7
+ module.exports = function getDeclarationValue (decl) {
8
+ const raws = decl.raws
9
+
10
+ return (raws.value && raws.value.raw) || decl.value
11
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @param {Node} node
3
+ * @returns {node is import('postcss').AtRule}
4
+ */
5
+ module.exports = function isAtRule (node) {
6
+ return node.type === 'atrule'
7
+ }
@@ -0,0 +1,10 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Check whether a property is a custom one
5
+ * @param {string} property
6
+ * @returns {boolean}
7
+ */
8
+ module.exports = function isCustomProperty (property) {
9
+ return property.startsWith('--')
10
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Check whether a property is standard
3
+ *
4
+ * @param {string} property
5
+ * @return {boolean} If `true`, the property is standard
6
+ */
7
+
8
+ module.exports = function isStandardSyntaxProperty (property) {
9
+ // SCSS var (e.g. $var: x), list (e.g. $list: (x)) or map (e.g. $map: (key:value))
10
+ if (property.startsWith('$')) {
11
+ return false
12
+ }
13
+
14
+ // Less var (e.g. @var: x)
15
+ if (property.startsWith('@')) {
16
+ return false
17
+ }
18
+
19
+ // SCSS or Less interpolation
20
+ if (/#{.+?}|@{.+?}|\$\(.+?\)/.test(property)) {
21
+ return false
22
+ }
23
+
24
+ return true
25
+ }