@primer/stylelint-config 13.0.0-rc.77d8c5f → 13.0.0-rc.7a254e8
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/dist/index.cjs +469 -175
- package/dist/index.mjs +469 -175
- package/package.json +10 -15
- package/plugins/borders.js +196 -63
- package/plugins/lib/utils.js +49 -0
- package/plugins/spacing.js +20 -23
- package/plugins/typography.js +185 -23
- package/plugins/lib/primitives.js +0 -36
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primer/stylelint-config",
|
|
3
|
-
"version": "13.0.0-rc.
|
|
3
|
+
"version": "13.0.0-rc.7a254e8",
|
|
4
4
|
"description": "Sharable stylelint config used by GitHub's CSS",
|
|
5
5
|
"author": "GitHub, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@github/browserslist-config": "^1.0.0",
|
|
47
47
|
"@primer/css": "^21.0.8",
|
|
48
|
-
"@primer/primitives": "^
|
|
48
|
+
"@primer/primitives": "^9.0.1",
|
|
49
49
|
"anymatch": "^3.1.1",
|
|
50
50
|
"postcss-scss": "^4.0.2",
|
|
51
51
|
"postcss-styled-syntax": "^0.6.4",
|
|
@@ -53,37 +53,32 @@
|
|
|
53
53
|
"string.prototype.matchall": "^4.0.2",
|
|
54
54
|
"stylelint": "^16.3.1",
|
|
55
55
|
"stylelint-config-standard": "^36.0.0",
|
|
56
|
-
"stylelint-css-modules-no-global-scoped-selector": "^1.0.2",
|
|
57
56
|
"stylelint-no-unsupported-browser-features": "^8.0.0",
|
|
58
57
|
"stylelint-order": "^6.0.4",
|
|
59
58
|
"stylelint-scss": "^6.2.0",
|
|
60
59
|
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
|
|
61
60
|
"tap-map": "^1.0.0"
|
|
62
61
|
},
|
|
63
|
-
"overrides": {
|
|
64
|
-
"stylelint-css-modules-no-global-scoped-selector": {
|
|
65
|
-
"stylelint": "$stylelint"
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
62
|
"prettier": "@github/prettier-config",
|
|
69
63
|
"devDependencies": {
|
|
70
64
|
"@changesets/changelog-github": "^0.5.0",
|
|
71
|
-
"@changesets/cli": "2.27.
|
|
65
|
+
"@changesets/cli": "2.27.7",
|
|
72
66
|
"@github/prettier-config": "^0.0.6",
|
|
73
|
-
"@rollup/plugin-commonjs": "^
|
|
67
|
+
"@rollup/plugin-commonjs": "^26.0.1",
|
|
74
68
|
"@rollup/plugin-json": "^6.1.0",
|
|
75
69
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
76
|
-
"@typescript-eslint/parser": "^
|
|
70
|
+
"@typescript-eslint/parser": "^8.0.1",
|
|
77
71
|
"dedent": "^1.5.3",
|
|
78
|
-
"eslint": "^8.0
|
|
79
|
-
"eslint-plugin-github": "^
|
|
72
|
+
"eslint": "^8.57.0",
|
|
73
|
+
"eslint-plugin-github": "^5.0.1",
|
|
74
|
+
"eslint-plugin-import": "^2.29.1",
|
|
80
75
|
"eslint-plugin-jest": "^28.2.0",
|
|
81
76
|
"eslint-plugin-prettier": "^5.1.3",
|
|
82
77
|
"jest": "^29.7.0",
|
|
83
78
|
"jest-preset-stylelint": "^7.0.0",
|
|
84
79
|
"prettier": "^3.2.5",
|
|
85
|
-
"rimraf": "^
|
|
86
|
-
"rollup": "^4.
|
|
80
|
+
"rimraf": "^6.0.1",
|
|
81
|
+
"rollup": "^4.21.1"
|
|
87
82
|
},
|
|
88
83
|
"jest": {
|
|
89
84
|
"transform": {},
|
package/plugins/borders.js
CHANGED
|
@@ -1,64 +1,197 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
'border color': {
|
|
28
|
-
expects: 'a border color variable',
|
|
29
|
-
props: 'border{,-top,-right,-bottom,-left}-color',
|
|
30
|
-
values: [
|
|
31
|
-
'$border-*',
|
|
32
|
-
'transparent',
|
|
33
|
-
'currentColor',
|
|
34
|
-
// Match variables in any of the following formats: --color-border-*, --color-*-border-*, --color-*-border, --borderColor-, *borderColor*
|
|
35
|
-
/var\(--color-(.+-)*border(-.+)*\)/,
|
|
36
|
-
/var\(--color-[^)]+\)/,
|
|
37
|
-
/var\(--borderColor-[^)]+\)/,
|
|
38
|
-
/var\((.+-)*borderColor(-.+)*\)/,
|
|
39
|
-
],
|
|
40
|
-
replacements: {
|
|
41
|
-
'$border-gray': '$border-color',
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
'border style': {
|
|
45
|
-
expects: 'a border style variable',
|
|
46
|
-
props: 'border{,-top,-right,-bottom,-left}-style',
|
|
47
|
-
values: ['$border-style', 'none'],
|
|
48
|
-
},
|
|
49
|
-
'border width': {
|
|
50
|
-
expects: 'a border width variable',
|
|
51
|
-
props: 'border{,-top,-right,-bottom,-left}-width',
|
|
52
|
-
values: ['$border-width*', '0'],
|
|
53
|
-
},
|
|
54
|
-
'border radius': {
|
|
55
|
-
expects: 'a border radius variable',
|
|
56
|
-
props: 'border{,-{top,bottom}-{left,right}}-radius',
|
|
57
|
-
values: ['$border-radius', '0', '50%', 'inherit'],
|
|
58
|
-
replacements: {
|
|
59
|
-
'100%': '50%',
|
|
60
|
-
},
|
|
61
|
-
},
|
|
1
|
+
import stylelint from 'stylelint'
|
|
2
|
+
import declarationValueIndex from 'stylelint/lib/utils/declarationValueIndex.cjs'
|
|
3
|
+
import valueParser from 'postcss-value-parser'
|
|
4
|
+
import {walkGroups, primitivesVariables} from './lib/utils.js'
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
createPlugin,
|
|
8
|
+
utils: {report, ruleMessages, validateOptions},
|
|
9
|
+
} = stylelint
|
|
10
|
+
|
|
11
|
+
export const ruleName = 'primer/borders'
|
|
12
|
+
export const messages = ruleMessages(ruleName, {
|
|
13
|
+
rejected: (value, replacement, propName) => {
|
|
14
|
+
if (propName && propName.includes('radius') && value.includes('borderWidth')) {
|
|
15
|
+
return `Border radius variables can not be used for border widths`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if ((propName && propName.includes('width')) || (borderShorthand(propName) && value.includes('borderRadius'))) {
|
|
19
|
+
return `Border width variables can not be used for border radii`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!replacement) {
|
|
23
|
+
return `Please use a Primer border variable instead of '${value}'. Consult the primer docs for a suitable replacement. https://primer.style/foundations/primitives/size#border`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return `Please replace '${value}' with a Primer border variable '${replacement['name']}'. https://primer.style/foundations/primitives/size#border`
|
|
62
27
|
},
|
|
63
|
-
|
|
64
|
-
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const variables = primitivesVariables('border')
|
|
31
|
+
const sizes = []
|
|
32
|
+
const radii = []
|
|
33
|
+
|
|
34
|
+
// Props that we want to check
|
|
35
|
+
const propList = ['border', 'border-width', 'border-radius']
|
|
36
|
+
// Values that we want to ignore
|
|
37
|
+
const valueList = ['${']
|
|
38
|
+
|
|
39
|
+
const borderShorthand = prop =>
|
|
40
|
+
/^border(-(top|right|bottom|left|block-start|block-end|inline-start|inline-end))?$/.test(prop)
|
|
41
|
+
|
|
42
|
+
for (const variable of variables) {
|
|
43
|
+
const name = variable['name']
|
|
44
|
+
|
|
45
|
+
if (name.includes('borderWidth')) {
|
|
46
|
+
const value = variable['values']
|
|
47
|
+
.pop()
|
|
48
|
+
.replace(/max|\(|\)/g, '')
|
|
49
|
+
.split(',')[0]
|
|
50
|
+
sizes.push({
|
|
51
|
+
name,
|
|
52
|
+
values: [value],
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (name.includes('borderRadius')) {
|
|
57
|
+
radii.push(variable)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @type {import('stylelint').Rule} */
|
|
62
|
+
const ruleFunction = (primary, secondaryOptions, context) => {
|
|
63
|
+
return (root, result) => {
|
|
64
|
+
const validOptions = validateOptions(result, ruleName, {
|
|
65
|
+
actual: primary,
|
|
66
|
+
possible: [true],
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
if (!validOptions) return
|
|
70
|
+
|
|
71
|
+
root.walkDecls(declNode => {
|
|
72
|
+
const {prop, value} = declNode
|
|
73
|
+
|
|
74
|
+
if (!propList.some(borderProp => prop.startsWith(borderProp))) return
|
|
75
|
+
if (/^border(-(top|right|bottom|left|block-start|block-end|inline-start|inline-end))?-color$/.test(prop)) return
|
|
76
|
+
if (valueList.some(valueToIgnore => value.includes(valueToIgnore))) return
|
|
77
|
+
|
|
78
|
+
const problems = []
|
|
79
|
+
|
|
80
|
+
const parsedValue = walkGroups(valueParser(value), node => {
|
|
81
|
+
const checkForVariable = (vars, nodeValue) =>
|
|
82
|
+
vars.some(variable =>
|
|
83
|
+
new RegExp(`${variable['name'].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(nodeValue),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// Only check word types. https://github.com/TrySound/postcss-value-parser#word
|
|
87
|
+
if (node.type !== 'word') {
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Exact values to ignore.
|
|
92
|
+
if (
|
|
93
|
+
[
|
|
94
|
+
'*',
|
|
95
|
+
'+',
|
|
96
|
+
'-',
|
|
97
|
+
'/',
|
|
98
|
+
'0',
|
|
99
|
+
'none',
|
|
100
|
+
'inherit',
|
|
101
|
+
'initial',
|
|
102
|
+
'revert',
|
|
103
|
+
'revert-layer',
|
|
104
|
+
'unset',
|
|
105
|
+
'solid',
|
|
106
|
+
'dashed',
|
|
107
|
+
'dotted',
|
|
108
|
+
'transparent',
|
|
109
|
+
].includes(node.value)
|
|
110
|
+
) {
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const valueUnit = valueParser.unit(node.value)
|
|
115
|
+
|
|
116
|
+
if (valueUnit && (valueUnit.unit === '' || !/^-?[0-9.]+$/.test(valueUnit.number))) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Skip if the value unit isn't a supported unit.
|
|
121
|
+
if (valueUnit && !['px', 'rem', 'em'].includes(valueUnit.unit)) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// if we're looking at the border property that sets color in shorthand, don't bother checking the color
|
|
126
|
+
if (
|
|
127
|
+
// using border shorthand
|
|
128
|
+
borderShorthand(prop) &&
|
|
129
|
+
// includes a color as a third space-separated value
|
|
130
|
+
value.split(' ').length > 2 &&
|
|
131
|
+
// the color in the third space-separated value includes `node.value`
|
|
132
|
+
value
|
|
133
|
+
.split(' ')
|
|
134
|
+
.slice(2)
|
|
135
|
+
.some(color => color.includes(node.value))
|
|
136
|
+
) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// If the variable is found in the value, skip it.
|
|
141
|
+
if (prop.includes('width') || borderShorthand(prop)) {
|
|
142
|
+
if (checkForVariable(sizes, node.value)) {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (prop.includes('radius')) {
|
|
148
|
+
if (checkForVariable(radii, node.value)) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const replacement = (prop.includes('radius') ? radii : sizes).find(variable =>
|
|
154
|
+
variable.values.includes(node.value.replace('-', '')),
|
|
155
|
+
)
|
|
156
|
+
const fixable = replacement && valueUnit && !valueUnit.number.includes('-')
|
|
157
|
+
|
|
158
|
+
if (fixable && context.fix) {
|
|
159
|
+
node.value = node.value.replace(node.value, `var(${replacement['name']})`)
|
|
160
|
+
} else {
|
|
161
|
+
problems.push({
|
|
162
|
+
index: declarationValueIndex(declNode) + node.sourceIndex,
|
|
163
|
+
endIndex: declarationValueIndex(declNode) + node.sourceIndex + node.value.length,
|
|
164
|
+
message: messages.rejected(node.value, replacement, prop),
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
if (context.fix) {
|
|
172
|
+
declNode.value = parsedValue.toString()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (problems.length) {
|
|
176
|
+
for (const err of problems) {
|
|
177
|
+
report({
|
|
178
|
+
index: err.index,
|
|
179
|
+
endIndex: err.endIndex,
|
|
180
|
+
message: err.message,
|
|
181
|
+
node: declNode,
|
|
182
|
+
result,
|
|
183
|
+
ruleName,
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
ruleFunction.ruleName = ruleName
|
|
192
|
+
ruleFunction.messages = messages
|
|
193
|
+
ruleFunction.meta = {
|
|
194
|
+
fixable: true,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export default createPlugin(ruleName, ruleFunction)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {createRequire} from 'node:module'
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url)
|
|
4
|
+
|
|
5
|
+
export function primitivesVariables(type) {
|
|
6
|
+
const variables = []
|
|
7
|
+
|
|
8
|
+
const files = []
|
|
9
|
+
switch (type) {
|
|
10
|
+
case 'spacing':
|
|
11
|
+
files.push('base/size/size.json')
|
|
12
|
+
break
|
|
13
|
+
case 'border':
|
|
14
|
+
files.push('functional/size/border.json')
|
|
15
|
+
break
|
|
16
|
+
case 'typography':
|
|
17
|
+
files.push('base/typography/typography.json')
|
|
18
|
+
files.push('functional/typography/typography.json')
|
|
19
|
+
break
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
// eslint-disable-next-line import/no-dynamic-require
|
|
24
|
+
const data = require(`@primer/primitives/dist/styleLint/${file}`)
|
|
25
|
+
|
|
26
|
+
for (const key of Object.keys(data)) {
|
|
27
|
+
const size = data[key]
|
|
28
|
+
const values = typeof size['value'] === 'string' ? [size['value']] : size['value']
|
|
29
|
+
|
|
30
|
+
variables.push({
|
|
31
|
+
name: `--${size['name']}`,
|
|
32
|
+
values,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return variables
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function walkGroups(root, validate) {
|
|
41
|
+
for (const node of root.nodes) {
|
|
42
|
+
if (node.type === 'function') {
|
|
43
|
+
walkGroups(node, validate)
|
|
44
|
+
} else {
|
|
45
|
+
validate(node)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return root
|
|
49
|
+
}
|
package/plugins/spacing.js
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
import stylelint from 'stylelint'
|
|
2
2
|
import declarationValueIndex from 'stylelint/lib/utils/declarationValueIndex.cjs'
|
|
3
3
|
import valueParser from 'postcss-value-parser'
|
|
4
|
-
import {primitivesVariables} from './lib/
|
|
4
|
+
import {primitivesVariables, walkGroups} from './lib/utils.js'
|
|
5
5
|
|
|
6
6
|
const {
|
|
7
7
|
createPlugin,
|
|
8
8
|
utils: {report, ruleMessages, validateOptions},
|
|
9
9
|
} = stylelint
|
|
10
10
|
|
|
11
|
-
const walkGroups = (root, validate) => {
|
|
12
|
-
for (const node of root.nodes) {
|
|
13
|
-
if (node.type === 'function') {
|
|
14
|
-
walkGroups(node, validate)
|
|
15
|
-
} else {
|
|
16
|
-
validate(node)
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return root
|
|
20
|
-
}
|
|
21
|
-
|
|
22
11
|
export const ruleName = 'primer/spacing'
|
|
23
12
|
export const messages = ruleMessages(ruleName, {
|
|
24
13
|
rejected: (value, replacement) => {
|
|
@@ -30,20 +19,26 @@ export const messages = ruleMessages(ruleName, {
|
|
|
30
19
|
},
|
|
31
20
|
})
|
|
32
21
|
|
|
33
|
-
|
|
34
|
-
|
|
22
|
+
// Props that we want to check
|
|
23
|
+
const propList = ['padding', 'margin', 'top', 'right', 'bottom', 'left']
|
|
24
|
+
// Values that we want to ignore
|
|
25
|
+
const valueList = ['${']
|
|
26
|
+
|
|
27
|
+
const sizes = primitivesVariables('spacing')
|
|
28
|
+
|
|
29
|
+
// Add +-1px to each value
|
|
30
|
+
for (const size of sizes) {
|
|
31
|
+
const values = size['values']
|
|
32
|
+
const px = parseInt(values.find(value => value.includes('px')))
|
|
33
|
+
if (![2, 6].includes(px)) {
|
|
34
|
+
values.push(`${px + 1}px`)
|
|
35
|
+
values.push(`${px - 1}px`)
|
|
36
|
+
}
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/** @type {import('stylelint').Rule} */
|
|
38
40
|
const ruleFunction = (primary, secondaryOptions, context) => {
|
|
39
|
-
return
|
|
40
|
-
// Props that we want to check
|
|
41
|
-
const propList = ['padding', 'margin', 'top', 'right', 'bottom', 'left']
|
|
42
|
-
// Values that we want to ignore
|
|
43
|
-
const valueList = ['${']
|
|
44
|
-
|
|
45
|
-
const sizes = await primitivesVariables('size')
|
|
46
|
-
|
|
41
|
+
return (root, result) => {
|
|
47
42
|
const validOptions = validateOptions(result, ruleName, {
|
|
48
43
|
actual: primary,
|
|
49
44
|
possible: [true],
|
|
@@ -128,6 +123,8 @@ const ruleFunction = (primary, secondaryOptions, context) => {
|
|
|
128
123
|
|
|
129
124
|
ruleFunction.ruleName = ruleName
|
|
130
125
|
ruleFunction.messages = messages
|
|
131
|
-
ruleFunction.meta =
|
|
126
|
+
ruleFunction.meta = {
|
|
127
|
+
fixable: true,
|
|
128
|
+
}
|
|
132
129
|
|
|
133
130
|
export default createPlugin(ruleName, ruleFunction)
|
package/plugins/typography.js
CHANGED
|
@@ -1,24 +1,186 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
1
|
+
import stylelint from 'stylelint'
|
|
2
|
+
import declarationValueIndex from 'stylelint/lib/utils/declarationValueIndex.cjs'
|
|
3
|
+
import {primitivesVariables} from './lib/utils.js'
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
createPlugin,
|
|
7
|
+
utils: {report, ruleMessages, validateOptions},
|
|
8
|
+
} = stylelint
|
|
9
|
+
|
|
10
|
+
export const ruleName = 'primer/typography'
|
|
11
|
+
export const messages = ruleMessages(ruleName, {
|
|
12
|
+
rejected: (value, replacement) => {
|
|
13
|
+
// no possible replacement
|
|
14
|
+
if (!replacement) {
|
|
15
|
+
return `Please use a Primer typography variable instead of '${value}'. Consult the primer docs for a suitable replacement. https://primer.style/foundations/primitives/typography`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// multiple possible replacements
|
|
19
|
+
if (replacement.length) {
|
|
20
|
+
return `Please use one of the following Primer typography variables instead of '${value}': ${replacement.map(replacementObj => `'${replacementObj.name}'`).join(', ')}. https://primer.style/foundations/primitives/typography`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// one possible replacement
|
|
24
|
+
return `Please replace '${value}' with Primer typography variable '${replacement['name']}'. https://primer.style/foundations/primitives/typography`
|
|
22
25
|
},
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const fontWeightKeywordMap = {
|
|
29
|
+
normal: 400,
|
|
30
|
+
bold: 600,
|
|
31
|
+
bolder: 600,
|
|
32
|
+
lighter: 300,
|
|
33
|
+
}
|
|
34
|
+
const getClosestFontWeight = (goalWeightNumber, fontWeightsTokens) => {
|
|
35
|
+
return fontWeightsTokens.reduce((prev, curr) =>
|
|
36
|
+
Math.abs(curr.values - goalWeightNumber) < Math.abs(prev.values - goalWeightNumber) ? curr : prev,
|
|
37
|
+
).values
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const variables = primitivesVariables('typography')
|
|
41
|
+
const fontSizes = []
|
|
42
|
+
const fontWeights = []
|
|
43
|
+
const lineHeights = []
|
|
44
|
+
const fontStacks = []
|
|
45
|
+
const fontShorthands = []
|
|
46
|
+
|
|
47
|
+
// Props that we want to check for typography variables
|
|
48
|
+
const propList = ['font-size', 'font-weight', 'line-height', 'font-family', 'font']
|
|
49
|
+
|
|
50
|
+
for (const variable of variables) {
|
|
51
|
+
const name = variable['name']
|
|
52
|
+
|
|
53
|
+
if (name.includes('size')) {
|
|
54
|
+
fontSizes.push(variable)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (name.includes('weight')) {
|
|
58
|
+
fontWeights.push(variable)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (name.includes('lineHeight')) {
|
|
62
|
+
lineHeights.push(variable)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (name.includes('fontStack')) {
|
|
66
|
+
fontStacks.push(variable)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (name.includes('shorthand')) {
|
|
70
|
+
fontShorthands.push(variable)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @type {import('stylelint').Rule} */
|
|
75
|
+
const ruleFunction = (primary, secondaryOptions, context) => {
|
|
76
|
+
return (root, result) => {
|
|
77
|
+
const validOptions = validateOptions(result, ruleName, {
|
|
78
|
+
actual: primary,
|
|
79
|
+
possible: [true],
|
|
80
|
+
})
|
|
81
|
+
let validValues = []
|
|
82
|
+
|
|
83
|
+
if (!validOptions) return
|
|
84
|
+
|
|
85
|
+
root.walkDecls(declNode => {
|
|
86
|
+
const {prop, value} = declNode
|
|
87
|
+
|
|
88
|
+
if (!propList.some(typographyProp => prop.startsWith(typographyProp))) return
|
|
89
|
+
|
|
90
|
+
const problems = []
|
|
91
|
+
|
|
92
|
+
const checkForVariable = (vars, nodeValue) =>
|
|
93
|
+
vars.some(variable =>
|
|
94
|
+
new RegExp(`${variable['name'].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(nodeValue),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
// Exact values to ignore.
|
|
98
|
+
if (value === 'inherit') {
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
switch (prop) {
|
|
103
|
+
case 'font-size':
|
|
104
|
+
validValues = fontSizes
|
|
105
|
+
break
|
|
106
|
+
case 'font-weight':
|
|
107
|
+
validValues = fontWeights
|
|
108
|
+
break
|
|
109
|
+
case 'line-height':
|
|
110
|
+
validValues = lineHeights
|
|
111
|
+
break
|
|
112
|
+
case 'font-family':
|
|
113
|
+
validValues = fontStacks
|
|
114
|
+
break
|
|
115
|
+
case 'font':
|
|
116
|
+
validValues = fontShorthands
|
|
117
|
+
break
|
|
118
|
+
default:
|
|
119
|
+
validValues = []
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (checkForVariable(validValues, value)) {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const getReplacements = () => {
|
|
127
|
+
const replacementTokens = validValues.filter(variable => {
|
|
128
|
+
if (!(variable.values instanceof Array)) {
|
|
129
|
+
let nodeValue = value
|
|
130
|
+
|
|
131
|
+
if (prop === 'font-weight') {
|
|
132
|
+
nodeValue = getClosestFontWeight(fontWeightKeywordMap[value] || value, fontWeights)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return variable.values.toString() === nodeValue.toString()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return variable.values.includes(value.replace('-', ''))
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if (!replacementTokens.length) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (replacementTokens.length > 1) {
|
|
146
|
+
return replacementTokens
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return replacementTokens[0]
|
|
150
|
+
}
|
|
151
|
+
const replacement = getReplacements()
|
|
152
|
+
const fixable = replacement && !replacement.length
|
|
153
|
+
|
|
154
|
+
if (fixable && context.fix) {
|
|
155
|
+
declNode.value = value.replace(value, `var(${replacement['name']})`)
|
|
156
|
+
} else {
|
|
157
|
+
problems.push({
|
|
158
|
+
index: declarationValueIndex(declNode),
|
|
159
|
+
endIndex: declarationValueIndex(declNode) + value.length,
|
|
160
|
+
message: messages.rejected(value, replacement, prop),
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (problems.length) {
|
|
165
|
+
for (const err of problems) {
|
|
166
|
+
report({
|
|
167
|
+
index: err.index,
|
|
168
|
+
endIndex: err.endIndex,
|
|
169
|
+
message: err.message,
|
|
170
|
+
node: declNode,
|
|
171
|
+
result,
|
|
172
|
+
ruleName,
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
ruleFunction.ruleName = ruleName
|
|
181
|
+
ruleFunction.messages = messages
|
|
182
|
+
ruleFunction.meta = {
|
|
183
|
+
fixable: true,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export default createPlugin(ruleName, ruleFunction)
|