@primer/stylelint-config 13.0.1 → 13.1.0-rc.437e7b4
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 +181 -356
- package/dist/index.mjs +179 -354
- package/package.json +2 -5
- package/plugins/colors.js +166 -41
- package/plugins/lib/utils.js +16 -0
- package/plugins/lib/decl-validator.js +0 -238
- package/plugins/lib/new-color-css-vars-map.json +0 -279
- package/plugins/lib/primitives-v8.json +0 -1482
- package/plugins/lib/variable-rules.js +0 -83
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primer/stylelint-config",
|
|
3
|
-
"version": "13.0.
|
|
3
|
+
"version": "13.1.0-rc.437e7b4",
|
|
4
4
|
"description": "Sharable stylelint config used by GitHub's CSS",
|
|
5
5
|
"author": "GitHub, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,9 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@github/browserslist-config": "^1.0.0",
|
|
47
|
-
"@primer/css": "^21.0.8",
|
|
48
47
|
"@primer/primitives": "^9.0.1",
|
|
49
|
-
"anymatch": "^3.1.1",
|
|
50
48
|
"postcss-scss": "^4.0.2",
|
|
51
49
|
"postcss-styled-syntax": "^0.6.4",
|
|
52
50
|
"postcss-value-parser": "^4.0.2",
|
|
@@ -56,8 +54,7 @@
|
|
|
56
54
|
"stylelint-no-unsupported-browser-features": "^8.0.0",
|
|
57
55
|
"stylelint-order": "^6.0.4",
|
|
58
56
|
"stylelint-scss": "^6.2.0",
|
|
59
|
-
"stylelint-value-no-unknown-custom-properties": "^6.0.1"
|
|
60
|
-
"tap-map": "^1.0.0"
|
|
57
|
+
"stylelint-value-no-unknown-custom-properties": "^6.0.1"
|
|
61
58
|
},
|
|
62
59
|
"prettier": "@github/prettier-config",
|
|
63
60
|
"devDependencies": {
|
package/plugins/colors.js
CHANGED
|
@@ -1,43 +1,168 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
/var\((.+-)*fgColor(-.+)*\)/,
|
|
11
|
-
/var\((.+-)*borderColor(-.+)*\)/,
|
|
12
|
-
/var\((.+-)*iconColor(-.+)*\)/,
|
|
13
|
-
]
|
|
1
|
+
import stylelint from 'stylelint'
|
|
2
|
+
import declarationValueIndex from 'stylelint/lib/utils/declarationValueIndex.cjs'
|
|
3
|
+
import {primitivesVariables, hasValidColor} from './lib/utils.js'
|
|
4
|
+
import valueParser from 'postcss-value-parser'
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
createPlugin,
|
|
8
|
+
utils: {report, ruleMessages, validateOptions},
|
|
9
|
+
} = stylelint
|
|
14
10
|
|
|
15
|
-
export
|
|
16
|
-
|
|
17
|
-
{
|
|
18
|
-
'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
'text color': {
|
|
27
|
-
expects: 'a text color variable',
|
|
28
|
-
props: 'color',
|
|
29
|
-
values: [
|
|
30
|
-
'$text-*',
|
|
31
|
-
'$tooltip-text-color',
|
|
32
|
-
'inherit',
|
|
33
|
-
// Match variables in any of the following formats: --color-text-*, --color-*-text-*, --color-*-text, *fgColor*, *iconColor*
|
|
34
|
-
/var\(--color-(.+-)*text(-.+)*\)/,
|
|
35
|
-
/var\(--color-(.+-)*fg(-.+)*\)/,
|
|
36
|
-
/var\(--color-[^)]+\)/,
|
|
37
|
-
/var\((.+-)*fgColor(-.+)*\)/,
|
|
38
|
-
/var\((.+-)*iconColor(-.+)*\)/,
|
|
39
|
-
],
|
|
40
|
-
},
|
|
11
|
+
export const ruleName = 'primer/colors'
|
|
12
|
+
export const messages = ruleMessages(ruleName, {
|
|
13
|
+
rejected: (value, type) => {
|
|
14
|
+
if (type === 'fg') {
|
|
15
|
+
return `Please use a Primer foreground color variable instead of '${value}'. https://primer.style/foundations/primitives/color#foreground`
|
|
16
|
+
} else if (type === 'bg') {
|
|
17
|
+
return `Please use a Primer background color variable instead of '${value}'. https://primer.style/foundations/primitives/color#background`
|
|
18
|
+
} else if (type === 'border') {
|
|
19
|
+
return `Please use a Primer border color variable instead of '${value}'. https://primer.style/foundations/primitives/color#border`
|
|
20
|
+
}
|
|
21
|
+
return `Please use with a Primer color variable instead of '${value}'. https://primer.style/foundations/primitives/color`
|
|
41
22
|
},
|
|
42
|
-
|
|
43
|
-
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
let variables = primitivesVariables('colors')
|
|
26
|
+
const validProps = {
|
|
27
|
+
'^color$': ['fgColor', 'iconColor'],
|
|
28
|
+
'^background(-color)?$': ['bgColor'],
|
|
29
|
+
'^border(-top|-right|-bottom|-left|-inline|-block)*(-color)?$': ['borderColor'],
|
|
30
|
+
'^fill$': ['fgColor', 'iconColor', 'bgColor'],
|
|
31
|
+
'^stroke$': ['fgColor', 'iconColor', 'bgColor', 'borderColor'],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const validValues = [
|
|
35
|
+
'none',
|
|
36
|
+
'currentcolor',
|
|
37
|
+
'inherit',
|
|
38
|
+
'initial',
|
|
39
|
+
'unset',
|
|
40
|
+
'revert',
|
|
41
|
+
'revert-layer',
|
|
42
|
+
'transparent',
|
|
43
|
+
'0',
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
const propType = prop => {
|
|
47
|
+
if (/^color/.test(prop)) {
|
|
48
|
+
return 'fg'
|
|
49
|
+
} else if (/^background(-color)?$/.test(prop)) {
|
|
50
|
+
return 'bg'
|
|
51
|
+
} else if (/^border(-top|-right|-bottom|-left|-inline|-block)*(-color)?$/.test(prop)) {
|
|
52
|
+
return 'border'
|
|
53
|
+
} else if (/^fill$/.test(prop)) {
|
|
54
|
+
return 'fg'
|
|
55
|
+
} else if (/^stroke$/.test(prop)) {
|
|
56
|
+
return 'fg'
|
|
57
|
+
}
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
variables = variables.filter(variable => {
|
|
62
|
+
const name = variable['name']
|
|
63
|
+
// remove shadow and boxShadow variables
|
|
64
|
+
return !(name.includes('shadow') || name.includes('boxShadow'))
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
/** @type {import('stylelint').Rule} */
|
|
68
|
+
const ruleFunction = primary => {
|
|
69
|
+
return (root, result) => {
|
|
70
|
+
const validOptions = validateOptions(result, ruleName, {
|
|
71
|
+
actual: primary,
|
|
72
|
+
possible: [true],
|
|
73
|
+
})
|
|
74
|
+
if (!validOptions) return
|
|
75
|
+
|
|
76
|
+
const valueIsCorrectType = (value, types) => types.some(type => value.includes(type))
|
|
77
|
+
|
|
78
|
+
root.walkDecls(declNode => {
|
|
79
|
+
const {prop, value} = declNode
|
|
80
|
+
|
|
81
|
+
// Skip if prop is not a valid color prop
|
|
82
|
+
if (!Object.keys(validProps).some(validProp => new RegExp(validProp).test(prop))) return
|
|
83
|
+
|
|
84
|
+
// Get the valid types for the prop
|
|
85
|
+
const types = validProps[Object.keys(validProps).find(re => new RegExp(re).test(prop))]
|
|
86
|
+
|
|
87
|
+
// Walk the value split
|
|
88
|
+
valueParser(value).walk(valueNode => {
|
|
89
|
+
// Skip if value is not a word or function
|
|
90
|
+
if (valueNode.type !== 'word' && valueNode.type !== 'function') return
|
|
91
|
+
|
|
92
|
+
// Skip if value is a valid value
|
|
93
|
+
if (validValues.includes(valueNode.value)) return
|
|
94
|
+
|
|
95
|
+
if (hasValidColor(valueNode.value) || /^\$/.test(valueNode.value)) {
|
|
96
|
+
const rejectedValue =
|
|
97
|
+
valueNode.type === 'function'
|
|
98
|
+
? `${valueNode.value}(${valueParser.stringify(valueNode.nodes)})`
|
|
99
|
+
: valueNode.value
|
|
100
|
+
|
|
101
|
+
report({
|
|
102
|
+
index: declarationValueIndex(declNode) + valueNode.sourceIndex,
|
|
103
|
+
endIndex: declarationValueIndex(declNode) + valueNode.sourceEndIndex,
|
|
104
|
+
message: messages.rejected(rejectedValue, propType(prop)),
|
|
105
|
+
node: declNode,
|
|
106
|
+
result,
|
|
107
|
+
ruleName,
|
|
108
|
+
})
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Skip functions
|
|
113
|
+
if (valueNode.type === 'function') {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Variable exists and is the correct type (fg, bg, border)
|
|
118
|
+
if (
|
|
119
|
+
variables.some(variable => new RegExp(variable['name']).test(valueNode.value)) &&
|
|
120
|
+
valueIsCorrectType(valueNode.value, types)
|
|
121
|
+
) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Value doesn't start with variable --
|
|
126
|
+
if (!valueNode.value.startsWith('--')) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Ignore old system colors --color-*
|
|
131
|
+
if (
|
|
132
|
+
[
|
|
133
|
+
/^--color-(?:[a-zA-Z0-9-]+-)*text(?:-[a-zA-Z0-9-]+)*$/,
|
|
134
|
+
/^--color-(?:[a-zA-Z0-9-](?!-))*-fg(?:-[a-zA-Z0-9-]+)*$/,
|
|
135
|
+
/^--color-[^)]+$/,
|
|
136
|
+
].some(oldSysRe => oldSysRe.test(valueNode.value))
|
|
137
|
+
) {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Property is shortand and value doesn't include color
|
|
142
|
+
if (
|
|
143
|
+
(/^border(-top|-right|-bottom|-left|-inline|-block)*$/.test(prop) || /^background$/.test(prop)) &&
|
|
144
|
+
!valueNode.value.toLowerCase().includes('color')
|
|
145
|
+
) {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
report({
|
|
150
|
+
index: declarationValueIndex(declNode) + valueNode.sourceIndex,
|
|
151
|
+
endIndex: declarationValueIndex(declNode) + valueNode.sourceEndIndex,
|
|
152
|
+
message: messages.rejected(`var(${valueNode.value})`, propType(prop)),
|
|
153
|
+
node: declNode,
|
|
154
|
+
result,
|
|
155
|
+
ruleName,
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
ruleFunction.ruleName = ruleName
|
|
163
|
+
ruleFunction.messages = messages
|
|
164
|
+
ruleFunction.meta = {
|
|
165
|
+
fixable: false,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export default createPlugin(ruleName, ruleFunction)
|
package/plugins/lib/utils.js
CHANGED
|
@@ -21,6 +21,9 @@ export function primitivesVariables(type) {
|
|
|
21
21
|
files.push('functional/themes/light.json')
|
|
22
22
|
files.push('functional/size/border.json')
|
|
23
23
|
break
|
|
24
|
+
case 'colors':
|
|
25
|
+
files.push('functional/themes/light.json')
|
|
26
|
+
break
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
for (const file of files) {
|
|
@@ -41,6 +44,19 @@ export function primitivesVariables(type) {
|
|
|
41
44
|
return variables
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
const HAS_VALID_HEX = /#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})(?:$|[^\da-f])/i
|
|
48
|
+
const COLOR_FUNCTION_NAMES = ['rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'lab', 'lch', 'oklab', 'oklch']
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if a value contains a valid 3, 4, 6 or 8 digit hex
|
|
52
|
+
*
|
|
53
|
+
* @param {string} value
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
export function hasValidColor(value) {
|
|
57
|
+
return HAS_VALID_HEX.test(value) || COLOR_FUNCTION_NAMES.includes(value)
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
export function walkGroups(root, validate) {
|
|
45
61
|
for (const node of root.nodes) {
|
|
46
62
|
if (node.type === 'function') {
|
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import anymatch from 'anymatch'
|
|
2
|
-
import valueParser from 'postcss-value-parser'
|
|
3
|
-
import TapMap from 'tap-map'
|
|
4
|
-
|
|
5
|
-
const SKIP_VALUE_NODE_TYPES = new Set(['space', 'div'])
|
|
6
|
-
const SKIP_AT_RULE_NAMES = new Set(['each', 'for', 'function', 'mixin'])
|
|
7
|
-
|
|
8
|
-
export default function declarationValidator(rules, options = {}) {
|
|
9
|
-
const {formatMessage = defaultMessageFormatter, variables, verbose = false} = options
|
|
10
|
-
const variableReplacements = new TapMap()
|
|
11
|
-
if (variables) {
|
|
12
|
-
for (const [name, {values}] of Object.entries(variables)) {
|
|
13
|
-
for (const value of values) {
|
|
14
|
-
variableReplacements.tap(value, () => []).push(name)
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const validators = Object.entries(rules)
|
|
20
|
-
.map(([key, rule]) => {
|
|
21
|
-
if (rule === false) {
|
|
22
|
-
return false
|
|
23
|
-
}
|
|
24
|
-
const {name = key, props = name, expects = `a ${name} value`} = rule
|
|
25
|
-
const replacements = Object.assign({}, rule.replacements, getVariableReplacements(rule.values))
|
|
26
|
-
// console.warn(`replacements for "${key}": ${JSON.stringify(replacements)}`)
|
|
27
|
-
Object.assign(rule, {name, props, expects, replacements})
|
|
28
|
-
return {
|
|
29
|
-
rule,
|
|
30
|
-
matchesProp: anymatch(props),
|
|
31
|
-
validate: Array.isArray(rule.components) ? componentValidator(rule) : valueValidator(rule),
|
|
32
|
-
}
|
|
33
|
-
})
|
|
34
|
-
.filter(Boolean)
|
|
35
|
-
|
|
36
|
-
const validatorsByProp = new TapMap()
|
|
37
|
-
const validatorsByReplacementValue = new Map()
|
|
38
|
-
for (const validator of validators) {
|
|
39
|
-
for (const value of Object.keys(validator.rule.replacements)) {
|
|
40
|
-
validatorsByReplacementValue.set(value, validator)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return decl => {
|
|
45
|
-
if (closest(decl, isSkippableAtRule)) {
|
|
46
|
-
if (verbose) {
|
|
47
|
-
// eslint-disable-next-line no-console
|
|
48
|
-
console.warn(`skipping declaration: ${decl.parent.toString()}`)
|
|
49
|
-
}
|
|
50
|
-
// As a general rule, any rule nested in an at-rule is ignored, since
|
|
51
|
-
// @for, @each, @mixin, and @function blocks can use whatever variables
|
|
52
|
-
// they want
|
|
53
|
-
return {valid: true}
|
|
54
|
-
}
|
|
55
|
-
const validator = getPropValidator(decl.prop)
|
|
56
|
-
if (validator) {
|
|
57
|
-
const result = validator.validate(decl)
|
|
58
|
-
result.errors = result.errors.map(formatMessage)
|
|
59
|
-
return result
|
|
60
|
-
} else {
|
|
61
|
-
return {valid: true}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function getVariableReplacements(values) {
|
|
66
|
-
const replacements = {}
|
|
67
|
-
const varValues = (Array.isArray(values) ? values : [values]).filter(v => typeof v === 'string' && v.includes('$'))
|
|
68
|
-
const matches = anymatch(varValues)
|
|
69
|
-
for (const [value, aliases] of variableReplacements.entries()) {
|
|
70
|
-
for (const alias of aliases) {
|
|
71
|
-
if (matches(alias)) {
|
|
72
|
-
replacements[value] = alias
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return replacements
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function getPropValidator(prop) {
|
|
80
|
-
return validatorsByProp.tap(prop, () => validators.find(v => v.matchesProp(prop)))
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function valueValidator({expects, values, replacements, singular = false}) {
|
|
84
|
-
const matches = anymatch(values)
|
|
85
|
-
return function validate({prop, value}, nested) {
|
|
86
|
-
if (matches(value)) {
|
|
87
|
-
return {
|
|
88
|
-
valid: true,
|
|
89
|
-
errors: [],
|
|
90
|
-
fixable: false,
|
|
91
|
-
replacement: undefined,
|
|
92
|
-
}
|
|
93
|
-
} else if (replacements[value]) {
|
|
94
|
-
let replacement = value
|
|
95
|
-
do {
|
|
96
|
-
replacement = replacements[replacement]
|
|
97
|
-
} while (replacements[replacement])
|
|
98
|
-
return {
|
|
99
|
-
valid: false,
|
|
100
|
-
errors: [{expects, prop, value, replacement}],
|
|
101
|
-
fixable: true,
|
|
102
|
-
replacement,
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
if (nested || singular) {
|
|
106
|
-
return {
|
|
107
|
-
valid: false,
|
|
108
|
-
errors: [{expects, prop, value}],
|
|
109
|
-
fixable: false,
|
|
110
|
-
replacement: undefined,
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const parsed = valueParser(value)
|
|
115
|
-
const validations = parsed.nodes
|
|
116
|
-
.map((node, index) => Object.assign(node, {index}))
|
|
117
|
-
.filter(node => !SKIP_VALUE_NODE_TYPES.has(node.type))
|
|
118
|
-
.map(node => {
|
|
119
|
-
const validation = validate({prop, value: valueParser.stringify(node)}, true)
|
|
120
|
-
validation.index = node.index
|
|
121
|
-
return validation
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
const valid = validations.every(v => v.valid)
|
|
125
|
-
if (valid) {
|
|
126
|
-
return {valid, errors: [], fixable: false, replacement: undefined}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const fixable = validations.some(v => v.fixable)
|
|
130
|
-
const errors = validations.reduce((list, v) => list.concat(v.errors), [])
|
|
131
|
-
|
|
132
|
-
let replacement = undefined
|
|
133
|
-
for (const validation of validations) {
|
|
134
|
-
if (fixable && validation.replacement) {
|
|
135
|
-
parsed.nodes[validation.index] = {type: 'word', value: validation.replacement}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (fixable) {
|
|
140
|
-
replacement = valueParser.stringify(parsed)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
valid,
|
|
145
|
-
fixable,
|
|
146
|
-
errors,
|
|
147
|
-
replacement,
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function componentValidator({expects, components, values, replacements}) {
|
|
154
|
-
const matchesCompoundValue = anymatch(values)
|
|
155
|
-
return decl => {
|
|
156
|
-
const {prop, value: compoundValue} = decl
|
|
157
|
-
const parsed = valueParser(compoundValue)
|
|
158
|
-
if (parsed.nodes.length === 1 && matchesCompoundValue(compoundValue)) {
|
|
159
|
-
return {valid: true, errors: []}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const errors = []
|
|
163
|
-
|
|
164
|
-
let fixable = false
|
|
165
|
-
let componentIndex = 0
|
|
166
|
-
for (const [index, node] of Object.entries(parsed.nodes)) {
|
|
167
|
-
if (SKIP_VALUE_NODE_TYPES.has(node.type)) {
|
|
168
|
-
continue
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const value = valueParser.stringify(node)
|
|
172
|
-
|
|
173
|
-
let componentProp = components[componentIndex++]
|
|
174
|
-
let validator = getPropValidator(componentProp)
|
|
175
|
-
if (validatorsByReplacementValue.has(value)) {
|
|
176
|
-
validator = validatorsByReplacementValue.get(value)
|
|
177
|
-
componentProp = validator.rule.name
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const nestedProp = `${componentProp} (in ${prop})`
|
|
181
|
-
if (validator) {
|
|
182
|
-
const result = validator.validate({prop: nestedProp, value}, true)
|
|
183
|
-
if (result.replacement) {
|
|
184
|
-
parsed.nodes[index] = {
|
|
185
|
-
type: 'word',
|
|
186
|
-
value: result.replacement,
|
|
187
|
-
}
|
|
188
|
-
fixable = true
|
|
189
|
-
}
|
|
190
|
-
for (const error of result.errors) {
|
|
191
|
-
errors.push(error)
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
errors.push({expects, prop: nestedProp, value})
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
let replacement = fixable ? valueParser.stringify(parsed) : undefined
|
|
199
|
-
|
|
200
|
-
// if a compound replacement exists, suggest *that* instead
|
|
201
|
-
if (replacement && replacements[replacement]) {
|
|
202
|
-
do {
|
|
203
|
-
replacement = replacements[replacement]
|
|
204
|
-
} while (replacements[replacement])
|
|
205
|
-
return {
|
|
206
|
-
valid: false,
|
|
207
|
-
errors: [{expects, prop, value: compoundValue, replacement}],
|
|
208
|
-
fixable: true,
|
|
209
|
-
replacement,
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return {
|
|
214
|
-
valid: errors.length === 0,
|
|
215
|
-
errors,
|
|
216
|
-
fixable,
|
|
217
|
-
replacement,
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function isSkippableAtRule(node) {
|
|
223
|
-
return node.type === 'atrule' && SKIP_AT_RULE_NAMES.has(node.name)
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function defaultMessageFormatter(error) {
|
|
228
|
-
const {expects, value, replacement} = error
|
|
229
|
-
const expected = replacement ? `"${replacement}"` : expects
|
|
230
|
-
return `Please use ${expected} instead of "${value}"`
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function closest(node, test) {
|
|
234
|
-
let ancestor = node
|
|
235
|
-
do {
|
|
236
|
-
if (test(ancestor)) return ancestor
|
|
237
|
-
} while ((ancestor = ancestor.parent))
|
|
238
|
-
}
|