@primer/stylelint-config 13.0.0-rc.fe9ab86 → 13.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.
@@ -1,22 +1,98 @@
1
- import {createVariableRule} from './lib/variable-rules.js'
2
-
3
- export default createVariableRule(
4
- 'primer/box-shadow',
5
- {
6
- 'box shadow': {
7
- expects: 'a box-shadow variable',
8
- props: 'box-shadow',
9
- values: [
10
- '$box-shadow*',
11
- '$*-shadow',
12
- 'none',
13
- // Match variables in any of the following formats: --color-shadow-*, --color-*-shadow-*, --color-*-shadow, --shadow-*, *shadow*
14
- /var\(--color-(.+-)*shadow(-.+)*\)/,
15
- /var\(--shadow(-.+)*\)/,
16
- /var\((.+-)*shadow(-.+)*\)/,
17
- ],
18
- singular: true,
19
- },
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/box-shadow'
11
+ export const messages = ruleMessages(ruleName, {
12
+ rejected: (value, replacement) => {
13
+ if (!replacement) {
14
+ return `Please use a Primer box-shadow variable instead of '${value}'. Consult the primer docs for a suitable replacement. https://primer.style/foundations/primitives/color#shadow or https://primer.style/foundations/primitives/size#border-size`
15
+ }
16
+
17
+ return `Please replace '${value}' with a Primer box-shadow variable '${replacement['name']}'. https://primer.style/foundations/primitives/color#shadow or https://primer.style/foundations/primitives/size#border-size`
20
18
  },
21
- 'https://primer.style/css/utilities/box-shadow',
22
- )
19
+ })
20
+
21
+ const variables = primitivesVariables('box-shadow')
22
+ const shadows = []
23
+
24
+ for (const variable of variables) {
25
+ const name = variable['name']
26
+
27
+ // TODO: Decide if this is safe. Someday we might have variables that
28
+ // have 'shadow' in the name but aren't full box-shadows.
29
+ if (name.includes('shadow') || name.includes('boxShadow')) {
30
+ shadows.push(variable)
31
+ }
32
+ }
33
+
34
+ /** @type {import('stylelint').Rule} */
35
+ const ruleFunction = (primary, secondaryOptions, context) => {
36
+ return (root, result) => {
37
+ const validOptions = validateOptions(result, ruleName, {
38
+ actual: primary,
39
+ possible: [true],
40
+ })
41
+ const validValues = shadows
42
+
43
+ if (!validOptions) return
44
+
45
+ root.walkDecls(declNode => {
46
+ const {prop, value} = declNode
47
+
48
+ if (prop !== 'box-shadow') return
49
+
50
+ if (value === 'none') return
51
+
52
+ const problems = []
53
+
54
+ const checkForVariable = (vars, nodeValue) => {
55
+ return vars.some(variable =>
56
+ new RegExp(`${variable['name'].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(nodeValue),
57
+ )
58
+ }
59
+
60
+ if (checkForVariable(validValues, value)) {
61
+ return
62
+ }
63
+
64
+ const replacement = validValues.find(variable => variable.values.includes(value))
65
+
66
+ if (replacement && context.fix) {
67
+ declNode.value = value.replace(value, `var(${replacement['name']})`)
68
+ } else {
69
+ problems.push({
70
+ index: declarationValueIndex(declNode),
71
+ endIndex: declarationValueIndex(declNode) + value.length,
72
+ message: messages.rejected(value, replacement),
73
+ })
74
+ }
75
+
76
+ if (problems.length) {
77
+ for (const err of problems) {
78
+ report({
79
+ index: err.index,
80
+ endIndex: err.endIndex,
81
+ message: err.message,
82
+ node: declNode,
83
+ result,
84
+ ruleName,
85
+ })
86
+ }
87
+ }
88
+ })
89
+ }
90
+ }
91
+
92
+ ruleFunction.ruleName = ruleName
93
+ ruleFunction.messages = messages
94
+ ruleFunction.meta = {
95
+ fixable: true,
96
+ }
97
+
98
+ export default createPlugin(ruleName, ruleFunction)
@@ -13,6 +13,14 @@ export function primitivesVariables(type) {
13
13
  case 'border':
14
14
  files.push('functional/size/border.json')
15
15
  break
16
+ case 'typography':
17
+ files.push('base/typography/typography.json')
18
+ files.push('functional/typography/typography.json')
19
+ break
20
+ case 'box-shadow':
21
+ files.push('functional/themes/light.json')
22
+ files.push('functional/size/border.json')
23
+ break
16
24
  }
17
25
 
18
26
  for (const file of files) {
@@ -1,24 +1,186 @@
1
- import {createVariableRule} from './lib/variable-rules.js'
2
-
3
- export default createVariableRule(
4
- 'primer/typography',
5
- {
6
- 'font-size': {
7
- expects: 'a font-size variable',
8
- values: ['$body-font-size', '$h{000,00,0,1,2,3,4,5,6}-size', '$font-size-*', '1', '1em', 'inherit'],
9
- },
10
- 'font-weight': {
11
- props: 'font-weight',
12
- values: ['$font-weight-*', 'inherit'],
13
- replacements: {
14
- bold: '$font-weight-bold',
15
- normal: '$font-weight-normal',
16
- },
17
- },
18
- 'line-height': {
19
- props: 'line-height',
20
- values: ['$body-line-height', '$lh-*', '0', '1', '1em', 'inherit'],
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
- 'https://primer.style/css/utilities/typography',
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)