stylelint-plugin-defensive-css 1.0.3 → 1.1.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/README.md +131 -6
- package/package.json +14 -14
- package/src/rules/use-defensive-css/accidental-hover/index.js +40 -0
- package/src/rules/use-defensive-css/background-repeat/index.js +73 -0
- package/src/{utils/findShorthandBackgroundRepeat.js → rules/use-defensive-css/background-repeat/utils.js} +1 -1
- package/src/rules/use-defensive-css/base.js +6 -0
- package/src/rules/use-defensive-css/custom-property-fallbacks/index.js +35 -0
- package/src/rules/use-defensive-css/flex-wrapping/index.js +47 -0
- package/src/rules/use-defensive-css/grid-line-names/index.js +89 -0
- package/src/rules/use-defensive-css/grid-line-names/utils.js +116 -0
- package/src/rules/use-defensive-css/index.js +19 -228
- package/src/rules/use-defensive-css/scroll-chaining/index.js +41 -0
- package/src/rules/use-defensive-css/scrollbar-gutter/index.js +41 -0
- package/src/rules/use-defensive-css/vendor-prefix-grouping/index.js +16 -0
- package/src/utils/overflow.js +7 -0
- /package/src/{utils/findCustomProperties.js → rules/use-defensive-css/custom-property-fallbacks/utils.js} +0 -0
- /package/src/{utils/findVendorPrefixes.js → rules/use-defensive-css/vendor-prefix-grouping/utils.js} +0 -0
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@ A Stylelint plugin to enforce defensive CSS best practices.
|
|
|
8
8
|
|
|
9
9
|
> [Read more about Defensive CSS](https://defensivecss.dev/)
|
|
10
10
|
|
|
11
|
-
## 🚀 Version 1.
|
|
11
|
+
## 🚀 Version 1.1.0
|
|
12
12
|
|
|
13
|
-
With the release of version 1.
|
|
13
|
+
With the release of version 1.1.0 of the plugin, we now support Stylelint 17.
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
@@ -49,9 +49,10 @@ The plugin provides multiple rules that can be toggled on and off as needed.
|
|
|
49
49
|
2. [Background-Repeat](#background-repeat)
|
|
50
50
|
3. [Custom Property Fallbacks](#custom-property-fallbacks)
|
|
51
51
|
4. [Flex Wrapping](#flex-wrapping)
|
|
52
|
-
5. [
|
|
53
|
-
6. [
|
|
54
|
-
7. [
|
|
52
|
+
5. [Grid Line Names](#grid-line-names)
|
|
53
|
+
6. [Scroll Chaining](#scroll-chaining)
|
|
54
|
+
7. [Scrollbar Gutter](#scrollbar-gutter)
|
|
55
|
+
8. [Vendor Prefix Grouping](#vendor-prefix-grouping)
|
|
55
56
|
|
|
56
57
|
---
|
|
57
58
|
|
|
@@ -146,6 +147,13 @@ div {
|
|
|
146
147
|
background: url('some-image.jpg') black top center;
|
|
147
148
|
background-repeat: no-repeat;
|
|
148
149
|
}
|
|
150
|
+
div {
|
|
151
|
+
mask: url('some-image.jpg') repeat top center;
|
|
152
|
+
}
|
|
153
|
+
div {
|
|
154
|
+
mask: url('some-image.jpg') top center;
|
|
155
|
+
mask-repeat: no-repeat;
|
|
156
|
+
}
|
|
149
157
|
```
|
|
150
158
|
|
|
151
159
|
#### ❌ Failing Examples
|
|
@@ -157,6 +165,12 @@ div {
|
|
|
157
165
|
div {
|
|
158
166
|
background-image: url('some-image.jpg');
|
|
159
167
|
}
|
|
168
|
+
div {
|
|
169
|
+
mask: url('some-image.jpg') top center;
|
|
170
|
+
}
|
|
171
|
+
div {
|
|
172
|
+
mask-image: url('some-image.jpg');
|
|
173
|
+
}
|
|
160
174
|
```
|
|
161
175
|
|
|
162
176
|
### Custom Property Fallbacks
|
|
@@ -286,6 +300,118 @@ div {
|
|
|
286
300
|
}
|
|
287
301
|
```
|
|
288
302
|
|
|
303
|
+
### Grid Line Names
|
|
304
|
+
|
|
305
|
+
Require explicit named grid lines for tracks. When `grid-line-names` is enabled
|
|
306
|
+
the plugin validates `grid-template-columns`, `grid-template-rows`, and the
|
|
307
|
+
`grid` shorthand (the portion before/after the `/`) to ensure each track is
|
|
308
|
+
associated with a named line using the `[name]` syntax.
|
|
309
|
+
|
|
310
|
+
The rule supports configuring whether to validate columns and/or rows:
|
|
311
|
+
|
|
312
|
+
```json
|
|
313
|
+
{
|
|
314
|
+
"rules": {
|
|
315
|
+
"plugin/use-defensive-css": [true, { "grid-line-names": true }]
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Or with explicit options:
|
|
321
|
+
|
|
322
|
+
```json
|
|
323
|
+
{
|
|
324
|
+
"rules": {
|
|
325
|
+
"plugin/use-defensive-css": [
|
|
326
|
+
true,
|
|
327
|
+
{ "grid-line-names": { "columns": true, "rows": false } }
|
|
328
|
+
]
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
- If `true` the rule validates both columns and rows.
|
|
334
|
+
- Pass an object with `columns` and/or `rows` set to `false` to disable one
|
|
335
|
+
side.
|
|
336
|
+
|
|
337
|
+
This rule helps avoid ambiguous layouts by rejecting unnamed tracks like
|
|
338
|
+
`1fr 1fr` and numeric `repeat(3, 1fr)` while allowing patterns that explicitly
|
|
339
|
+
name lines, e.g. `repeat(auto-fit, [name] 300px)` or bracketed names such as
|
|
340
|
+
`[a b] 1fr`.
|
|
341
|
+
|
|
342
|
+
#### ✅ Passing Examples
|
|
343
|
+
|
|
344
|
+
```css
|
|
345
|
+
div {
|
|
346
|
+
grid-template-columns: [c-a] 1fr [c-b] 1fr;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
div {
|
|
350
|
+
grid-template-rows: [r-a] 1fr [r-b] 2fr;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
div {
|
|
354
|
+
grid-template-columns: [a] [b] 1fr [c] 2fr;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
div {
|
|
358
|
+
grid-template-columns: repeat(auto-fit, [line-a line-b] 300px);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
div {
|
|
362
|
+
grid-template-rows: repeat(auto-fill, [r1 r2] 100px);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
div {
|
|
366
|
+
grid: [r-a] 1fr / [c-a] 1fr [c-b] 2fr;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
div {
|
|
370
|
+
grid-template-columns: repeat(auto-fit, [a]300px);
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
#### ❌ Failing Examples
|
|
375
|
+
|
|
376
|
+
```css
|
|
377
|
+
div {
|
|
378
|
+
grid-template-columns: 1fr 1fr;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
div {
|
|
382
|
+
grid-template-rows: 1fr 1fr;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
div {
|
|
386
|
+
grid-template-columns: repeat(3, 1fr);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
div {
|
|
390
|
+
grid-template-rows: repeat(3, 1fr);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
div {
|
|
394
|
+
grid: auto / 1fr 1fr;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
div {
|
|
398
|
+
grid: repeat(3, 1fr) / auto;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
div {
|
|
402
|
+
grid-template-columns: 1fr [after] 1fr;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* Reserved identifiers cannot be used as line names */
|
|
406
|
+
div {
|
|
407
|
+
grid-template-columns: [auto] 1fr;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
div {
|
|
411
|
+
grid-template-rows: [span] 1fr;
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
289
415
|
### Scroll Chaining
|
|
290
416
|
|
|
291
417
|
> [Read more about this pattern in Defensive CSS](https://defensivecss.dev/tip/scroll-chain/)
|
|
@@ -432,7 +558,6 @@ input::-webkit-input-placeholder {
|
|
|
432
558
|
}
|
|
433
559
|
input::-moz-placeholder {
|
|
434
560
|
color: #222;
|
|
435
|
-
}
|
|
436
561
|
```
|
|
437
562
|
|
|
438
563
|
#### ❌ Failing Examples
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stylelint-plugin-defensive-css",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A Stylelint plugin to enforce defensive CSS best practices.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -41,19 +41,19 @@
|
|
|
41
41
|
"stylelint": "^14.0.0 || ^15.0.0 || ^16.0.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@commitlint/cli": "^
|
|
45
|
-
"@commitlint/config-conventional": "^
|
|
46
|
-
"cross-env": "^
|
|
47
|
-
"eslint": "^8.
|
|
48
|
-
"husky": "^
|
|
49
|
-
"jest": "^
|
|
50
|
-
"jest-cli": "^
|
|
51
|
-
"jest-light-runner": "^0.
|
|
52
|
-
"jest-preset-stylelint": "^
|
|
53
|
-
"lint-staged": "^
|
|
54
|
-
"prettier": "^3.
|
|
55
|
-
"prettier-eslint": "^16.
|
|
56
|
-
"stylelint": "^
|
|
44
|
+
"@commitlint/cli": "^20.4.0",
|
|
45
|
+
"@commitlint/config-conventional": "^20.4.0",
|
|
46
|
+
"cross-env": "^10.1.0",
|
|
47
|
+
"eslint": "^8.57.1",
|
|
48
|
+
"husky": "^9.1.7",
|
|
49
|
+
"jest": "^30.2.0",
|
|
50
|
+
"jest-cli": "^30.2.0",
|
|
51
|
+
"jest-light-runner": "^0.7.11",
|
|
52
|
+
"jest-preset-stylelint": "^9.1.0",
|
|
53
|
+
"lint-staged": "^16.2.7",
|
|
54
|
+
"prettier": "^3.8.1",
|
|
55
|
+
"prettier-eslint": "^16.4.2",
|
|
56
|
+
"stylelint": "^17.1.0"
|
|
57
57
|
},
|
|
58
58
|
"lint-staged": {
|
|
59
59
|
"**/*.js|md|json": [
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import stylelint from 'stylelint';
|
|
2
|
+
import { ruleMessages, ruleName } from '../base.js';
|
|
3
|
+
|
|
4
|
+
export function accidentalHover({ decl, result }) {
|
|
5
|
+
let isWrappedInHoverAtRule = false;
|
|
6
|
+
|
|
7
|
+
function traverseParentRules(parent) {
|
|
8
|
+
if (parent.parent.type === 'root') {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (parent.parent.params && /hover(: hover)?/.test(parent.parent.params)) {
|
|
13
|
+
isWrappedInHoverAtRule = true;
|
|
14
|
+
} else {
|
|
15
|
+
traverseParentRules(parent.parent);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const parent = decl.parent;
|
|
20
|
+
const selector = parent.selector;
|
|
21
|
+
const isHoverSelector = selector?.includes(':hover');
|
|
22
|
+
|
|
23
|
+
// If the :hover selector is inside a :not() selector, ignore it
|
|
24
|
+
if (/:not\(([^)]*:hover[^)]*)\)/g.test(selector)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (isHoverSelector) {
|
|
29
|
+
traverseParentRules(parent);
|
|
30
|
+
|
|
31
|
+
if (!isWrappedInHoverAtRule) {
|
|
32
|
+
stylelint.utils.report({
|
|
33
|
+
message: ruleMessages.accidentalHover(),
|
|
34
|
+
node: decl.parent,
|
|
35
|
+
result,
|
|
36
|
+
ruleName,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import stylelint from 'stylelint';
|
|
2
|
+
import { ruleMessages, ruleName } from '../base.js';
|
|
3
|
+
import { findShorthandBackgroundRepeat } from './utils.js';
|
|
4
|
+
|
|
5
|
+
const defaultBackgroundRepeatProps = {
|
|
6
|
+
hasBackgroundImage: false,
|
|
7
|
+
isMissingBackgroundRepeat: true,
|
|
8
|
+
nodeToReport: undefined,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const defaultMaskRepeatProps = {
|
|
12
|
+
hasMaskImage: false,
|
|
13
|
+
isMissingMaskRepeat: true,
|
|
14
|
+
nodeToReport: undefined,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let backgroundRepeatProps = { ...defaultBackgroundRepeatProps };
|
|
18
|
+
let maskRepeatProps = { ...defaultMaskRepeatProps };
|
|
19
|
+
|
|
20
|
+
export function backgroundRepeat({ decl, isLastStyleDeclaration, result }) {
|
|
21
|
+
const hasUrl = decl.value.includes('url(');
|
|
22
|
+
if (decl.prop === 'background' && hasUrl) {
|
|
23
|
+
backgroundRepeatProps.hasBackgroundImage = true;
|
|
24
|
+
backgroundRepeatProps.isMissingBackgroundRepeat =
|
|
25
|
+
!findShorthandBackgroundRepeat(decl.value);
|
|
26
|
+
backgroundRepeatProps.nodeToReport = decl;
|
|
27
|
+
}
|
|
28
|
+
if (decl.prop === 'mask' && hasUrl) {
|
|
29
|
+
maskRepeatProps.hasMaskImage = true;
|
|
30
|
+
maskRepeatProps.isMissingMaskRepeat = !findShorthandBackgroundRepeat(
|
|
31
|
+
decl.value,
|
|
32
|
+
);
|
|
33
|
+
maskRepeatProps.nodeToReport = decl;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (decl.prop === 'background-image' && hasUrl) {
|
|
37
|
+
backgroundRepeatProps.hasBackgroundImage = true;
|
|
38
|
+
backgroundRepeatProps.nodeToReport = decl;
|
|
39
|
+
}
|
|
40
|
+
if (decl.prop === 'mask-image' && hasUrl) {
|
|
41
|
+
maskRepeatProps.hasMaskImage = true;
|
|
42
|
+
maskRepeatProps.nodeToReport = decl;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (decl.prop === 'background-repeat') {
|
|
46
|
+
backgroundRepeatProps.isMissingBackgroundRepeat = false;
|
|
47
|
+
}
|
|
48
|
+
if (decl.prop === 'mask-repeat') {
|
|
49
|
+
maskRepeatProps.isMissingMaskRepeat = false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (isLastStyleDeclaration) {
|
|
53
|
+
if (Object.values(backgroundRepeatProps).every((prop) => prop)) {
|
|
54
|
+
stylelint.utils.report({
|
|
55
|
+
message: ruleMessages.backgroundRepeat(),
|
|
56
|
+
node: backgroundRepeatProps.nodeToReport,
|
|
57
|
+
result,
|
|
58
|
+
ruleName,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (Object.values(maskRepeatProps).every((prop) => prop)) {
|
|
62
|
+
stylelint.utils.report({
|
|
63
|
+
message: ruleMessages.maskRepeat(),
|
|
64
|
+
node: maskRepeatProps.nodeToReport,
|
|
65
|
+
result,
|
|
66
|
+
ruleName,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
backgroundRepeatProps = { ...defaultBackgroundRepeatProps };
|
|
71
|
+
maskRepeatProps = { ...defaultMaskRepeatProps };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -15,6 +15,12 @@ export const ruleMessages = stylelint.utils.ruleMessages(ruleName, {
|
|
|
15
15
|
flexWrapping() {
|
|
16
16
|
return 'Whenever setting an element to `display: flex` a `flex-wrap` value must be defined. Set `flex-wrap: nowrap` for the default behavior. Learn more: https://defensivecss.dev/tip/flexbox-wrapping/';
|
|
17
17
|
},
|
|
18
|
+
gridLineNames() {
|
|
19
|
+
return 'When defining grid tracks, name each grid line using the [name] syntax so each track is preceded by a named line. This applies to both rows and columns (depending on configuration). Avoid unnamed tracks like `1fr 1fr` or numeric repeat counts like `repeat(3, 1fr)`. Learn more: https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template';
|
|
20
|
+
},
|
|
21
|
+
maskRepeat() {
|
|
22
|
+
return 'Whenever setting a mask image, be sure to explicitly define a `mask-repeat` value. Learn more: https://defensivecss.dev/tip/bg-repeat/';
|
|
23
|
+
},
|
|
18
24
|
scrollChaining() {
|
|
19
25
|
return 'To prevent scroll chaining between contexts, any container with a scrollable overflow must have a `overscroll-behavior` value defined. Learn more: https://defensivecss.dev/tip/scroll-chain/';
|
|
20
26
|
},
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import stylelint from 'stylelint';
|
|
2
|
+
import { ruleMessages, ruleName } from '../base.js';
|
|
3
|
+
import { findCustomProperties } from './utils.js';
|
|
4
|
+
|
|
5
|
+
export function customPropertyFallbacks({ decl, options, result }) {
|
|
6
|
+
const propertiesWithoutFallback = findCustomProperties(decl.value);
|
|
7
|
+
|
|
8
|
+
if (propertiesWithoutFallback.length) {
|
|
9
|
+
if (Array.isArray(options?.['custom-property-fallbacks'])) {
|
|
10
|
+
if (options['custom-property-fallbacks'][0]) {
|
|
11
|
+
const patterns = options['custom-property-fallbacks'][1].ignore;
|
|
12
|
+
const patternMatched = propertiesWithoutFallback.some((property) => {
|
|
13
|
+
return patterns.some((pattern) =>
|
|
14
|
+
typeof pattern === 'string'
|
|
15
|
+
? new RegExp(pattern).test(property)
|
|
16
|
+
: pattern.test(property),
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (patternMatched) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
stylelint.utils.report({
|
|
29
|
+
message: ruleMessages.customPropertyFallbacks(),
|
|
30
|
+
node: decl,
|
|
31
|
+
result,
|
|
32
|
+
ruleName,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import stylelint from 'stylelint';
|
|
2
|
+
import { ruleMessages, ruleName } from '../base.js';
|
|
3
|
+
|
|
4
|
+
const defaultFlexWrappingProps = {
|
|
5
|
+
isDisplayFlex: false,
|
|
6
|
+
isFlexRow: true,
|
|
7
|
+
isMissingFlexWrap: true,
|
|
8
|
+
nodeToReport: undefined,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
let flexWrappingProps = { ...defaultFlexWrappingProps };
|
|
12
|
+
|
|
13
|
+
export function flexWrapping({ decl, isLastStyleDeclaration, result }) {
|
|
14
|
+
if (decl.prop === 'display' && decl.value.includes('flex')) {
|
|
15
|
+
flexWrappingProps.isDisplayFlex = true;
|
|
16
|
+
flexWrappingProps.nodeToReport = decl;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (decl.prop === 'flex-flow' && decl.value.includes('column')) {
|
|
20
|
+
flexWrappingProps.isFlexRow = false;
|
|
21
|
+
flexWrappingProps.isMissingFlexWrap = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (decl.prop === 'flex-direction' && decl.value.includes('column')) {
|
|
25
|
+
flexWrappingProps.isFlexRow = false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
decl.prop === 'flex-wrap' ||
|
|
30
|
+
(decl.prop === 'flex-flow' && decl.value.includes('wrap'))
|
|
31
|
+
) {
|
|
32
|
+
flexWrappingProps.isMissingFlexWrap = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (isLastStyleDeclaration) {
|
|
36
|
+
if (Object.values(flexWrappingProps).every((prop) => prop)) {
|
|
37
|
+
stylelint.utils.report({
|
|
38
|
+
message: ruleMessages.flexWrapping(),
|
|
39
|
+
node: flexWrappingProps.nodeToReport,
|
|
40
|
+
result,
|
|
41
|
+
ruleName,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
flexWrappingProps = { ...defaultFlexWrappingProps };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import stylelint from 'stylelint';
|
|
2
|
+
import { ruleMessages, ruleName } from '../base.js';
|
|
3
|
+
import { isValueInvalidForLineNames, tokenizeGridColumns } from './utils.js';
|
|
4
|
+
|
|
5
|
+
export function gridLineNames({ decl, options, result }) {
|
|
6
|
+
const opt = options['grid-line-names'];
|
|
7
|
+
let validateColumns = false;
|
|
8
|
+
let validateRows = false;
|
|
9
|
+
|
|
10
|
+
if (opt === true) {
|
|
11
|
+
validateColumns = validateRows = true;
|
|
12
|
+
} else if (Array.isArray(opt)) {
|
|
13
|
+
const cfg = opt[1] || {};
|
|
14
|
+
validateColumns = cfg.columns !== false;
|
|
15
|
+
validateRows = cfg.rows !== false;
|
|
16
|
+
} else if (typeof opt === 'object') {
|
|
17
|
+
validateColumns = opt.columns !== false;
|
|
18
|
+
validateRows = opt.rows !== false;
|
|
19
|
+
} else {
|
|
20
|
+
// unknown shape — skip
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Determine which declarations to validate
|
|
25
|
+
if (decl.prop === 'grid-template-columns' && validateColumns) {
|
|
26
|
+
if (isValueInvalidForLineNames(decl.value)) {
|
|
27
|
+
stylelint.utils.report({
|
|
28
|
+
message: ruleMessages.gridLineNames(),
|
|
29
|
+
node: decl,
|
|
30
|
+
result,
|
|
31
|
+
ruleName,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (decl.prop === 'grid-template-rows' && validateRows) {
|
|
37
|
+
if (isValueInvalidForLineNames(decl.value)) {
|
|
38
|
+
stylelint.utils.report({
|
|
39
|
+
message: ruleMessages.gridLineNames(),
|
|
40
|
+
node: decl,
|
|
41
|
+
result,
|
|
42
|
+
ruleName,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (decl.prop === 'grid') {
|
|
48
|
+
if (!validateColumns && !validateRows) return;
|
|
49
|
+
|
|
50
|
+
const value = decl.value;
|
|
51
|
+
const slashIndex = value.indexOf('/');
|
|
52
|
+
|
|
53
|
+
// If there's no slash we can't reliably extract rows/columns
|
|
54
|
+
if (slashIndex === -1) return;
|
|
55
|
+
|
|
56
|
+
const rowsValue = value.slice(0, slashIndex).trim();
|
|
57
|
+
const colsValue = value.slice(slashIndex + 1).trim();
|
|
58
|
+
|
|
59
|
+
let invalidRows = false;
|
|
60
|
+
let invalidCols = false;
|
|
61
|
+
|
|
62
|
+
if (validateRows) {
|
|
63
|
+
const rowTokens = tokenizeGridColumns(rowsValue || '');
|
|
64
|
+
const skipRowsValidation =
|
|
65
|
+
rowTokens.length === 1 &&
|
|
66
|
+
!/^\[[^\]]+\]$/.test(rowTokens[0]) &&
|
|
67
|
+
!/repeat\s*\(/i.test(rowTokens[0]);
|
|
68
|
+
|
|
69
|
+
if (!skipRowsValidation && isValueInvalidForLineNames(rowsValue)) {
|
|
70
|
+
invalidRows = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (validateColumns) {
|
|
75
|
+
if (isValueInvalidForLineNames(colsValue)) {
|
|
76
|
+
invalidCols = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (invalidRows || invalidCols) {
|
|
81
|
+
stylelint.utils.report({
|
|
82
|
+
message: ruleMessages.gridLineNames(),
|
|
83
|
+
node: decl,
|
|
84
|
+
result,
|
|
85
|
+
ruleName,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export function tokenizeGridColumns(value) {
|
|
2
|
+
const tokens = [];
|
|
3
|
+
let cur = '';
|
|
4
|
+
let inBracket = false;
|
|
5
|
+
let inParens = false;
|
|
6
|
+
|
|
7
|
+
for (let i = 0; i < value.length; i++) {
|
|
8
|
+
const ch = value[i];
|
|
9
|
+
|
|
10
|
+
if (ch === '[') {
|
|
11
|
+
if (cur.trim()) {
|
|
12
|
+
tokens.push(cur.trim());
|
|
13
|
+
cur = '';
|
|
14
|
+
}
|
|
15
|
+
inBracket = true;
|
|
16
|
+
cur += ch;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (ch === ']') {
|
|
21
|
+
cur += ch;
|
|
22
|
+
inBracket = false;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (ch === '(') {
|
|
27
|
+
inParens = true;
|
|
28
|
+
cur += ch;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (ch === ')') {
|
|
33
|
+
cur += ch;
|
|
34
|
+
inParens = false;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!inBracket && !inParens && /\s/.test(ch)) {
|
|
39
|
+
if (cur.trim()) {
|
|
40
|
+
tokens.push(cur.trim());
|
|
41
|
+
cur = '';
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
cur += ch;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (cur.trim()) tokens.push(cur.trim());
|
|
50
|
+
return tokens;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Extract names from the first bracketed group within a token, or `null`.
|
|
54
|
+
export function extractBracketNames(token) {
|
|
55
|
+
const m = token.match(/\[([^\]]+)\]/);
|
|
56
|
+
if (!m) return null;
|
|
57
|
+
return m[1].trim().split(/\s+/);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function extractAllBracketNamesFromString(str) {
|
|
61
|
+
const names = [];
|
|
62
|
+
const re = /\[([^\]]+)\]/g;
|
|
63
|
+
let m;
|
|
64
|
+
while ((m = re.exec(str))) {
|
|
65
|
+
names.push(...m[1].trim().split(/\s+/));
|
|
66
|
+
}
|
|
67
|
+
return names;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildTrackTokens(tokens) {
|
|
71
|
+
const trackTokens = [];
|
|
72
|
+
for (let i = 0; i < tokens.length; ) {
|
|
73
|
+
const token = tokens[i];
|
|
74
|
+
|
|
75
|
+
if (/^\[[^\]]+\]$/.test(token)) {
|
|
76
|
+
const brackets = [token];
|
|
77
|
+
i++;
|
|
78
|
+
while (i < tokens.length && /^\[[^\]]+\]$/.test(tokens[i])) {
|
|
79
|
+
brackets.push(tokens[i]);
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (i < tokens.length) {
|
|
84
|
+
trackTokens.push(`${brackets.join(' ')} ${tokens[i]}`);
|
|
85
|
+
i++;
|
|
86
|
+
} else {
|
|
87
|
+
trackTokens.push(brackets.join(' '));
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
trackTokens.push(token);
|
|
91
|
+
i++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return trackTokens;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function isValueInvalidForLineNames(value) {
|
|
98
|
+
const tokens = tokenizeGridColumns(value);
|
|
99
|
+
const trackTokens = buildTrackTokens(tokens);
|
|
100
|
+
|
|
101
|
+
if (!trackTokens.length) return false;
|
|
102
|
+
|
|
103
|
+
const invalidTrack = trackTokens.find((token) => {
|
|
104
|
+
let names = extractBracketNames(token);
|
|
105
|
+
|
|
106
|
+
if ((!names || !names.length) && /repeat\s*\(/i.test(token)) {
|
|
107
|
+
names = extractAllBracketNamesFromString(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!names || !names.length) return true;
|
|
111
|
+
|
|
112
|
+
return names.some((n) => ['span', 'auto'].includes(n));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return Boolean(invalidTrack);
|
|
116
|
+
}
|
|
@@ -1,60 +1,17 @@
|
|
|
1
1
|
import stylelint from 'stylelint';
|
|
2
2
|
|
|
3
3
|
import { ruleName, ruleMessages, ruleMeta } from './base.js';
|
|
4
|
-
import { findShorthandBackgroundRepeat } from '../../utils/findShorthandBackgroundRepeat.js';
|
|
5
|
-
import { findVendorPrefixes } from '../../utils/findVendorPrefixes.js';
|
|
6
|
-
import { findCustomProperties } from '../../utils/findCustomProperties.js';
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
isMissingFlexWrap: true,
|
|
17
|
-
nodeToReport: undefined,
|
|
18
|
-
};
|
|
19
|
-
const defaultScrollbarGutterProps = {
|
|
20
|
-
hasOverflow: false,
|
|
21
|
-
hasScrollbarGutter: false,
|
|
22
|
-
nodeToReport: undefined,
|
|
23
|
-
};
|
|
24
|
-
const defaultScrollChainingProps = {
|
|
25
|
-
hasOverflow: false,
|
|
26
|
-
hasOverscrollBehavior: false,
|
|
27
|
-
nodeToReport: undefined,
|
|
28
|
-
};
|
|
5
|
+
import { accidentalHover } from './accidental-hover/index.js';
|
|
6
|
+
import { backgroundRepeat } from './background-repeat/index.js';
|
|
7
|
+
import { customPropertyFallbacks } from './custom-property-fallbacks/index.js';
|
|
8
|
+
import { flexWrapping } from './flex-wrapping/index.js';
|
|
9
|
+
import { gridLineNames } from './grid-line-names/index.js';
|
|
10
|
+
import { scrollChaining } from './scroll-chaining/index.js';
|
|
11
|
+
import { scrollbarGutter } from './scrollbar-gutter/index.js';
|
|
12
|
+
import { vendorPrefixGrouping } from './vendor-prefix-grouping/index.js';
|
|
29
13
|
|
|
30
|
-
let backgroundRepeatProps = { ...defaultBackgroundRepeatProps };
|
|
31
|
-
let flexWrappingProps = { ...defaultFlexWrappingProps };
|
|
32
|
-
let scrollbarGutterProps = { ...defaultScrollbarGutterProps };
|
|
33
|
-
let scrollChainingProps = { ...defaultScrollChainingProps };
|
|
34
14
|
let isLastStyleDeclaration = false;
|
|
35
|
-
let isWrappedInHoverAtRule = false;
|
|
36
|
-
|
|
37
|
-
const overflowProperties = [
|
|
38
|
-
'overflow',
|
|
39
|
-
'overflow-x',
|
|
40
|
-
'overflow-y',
|
|
41
|
-
'overflow-inline',
|
|
42
|
-
'overflow-block',
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
function traverseParentRules(parent) {
|
|
46
|
-
if (parent.parent.type === 'root') {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (parent.parent.type === 'atrule') {
|
|
51
|
-
if (parent.parent.params && /hover(: hover)?/.test(parent.parent.params)) {
|
|
52
|
-
isWrappedInHoverAtRule = true;
|
|
53
|
-
} else {
|
|
54
|
-
traverseParentRules(parent.parent);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
15
|
|
|
59
16
|
const ruleFunction = (_, options) => {
|
|
60
17
|
return (root, result) => {
|
|
@@ -71,208 +28,42 @@ const ruleFunction = (_, options) => {
|
|
|
71
28
|
|
|
72
29
|
/* ACCIDENTAL HOVER */
|
|
73
30
|
if (options?.['accidental-hover']) {
|
|
74
|
-
|
|
75
|
-
const selector = parent.selector;
|
|
76
|
-
const isHoverSelector = selector?.includes(':hover');
|
|
77
|
-
isWrappedInHoverAtRule = false;
|
|
78
|
-
|
|
79
|
-
// If the :hover selector is inside a :not() selector, ignore it
|
|
80
|
-
if (/:not\(([^)]*:hover[^)]*)\)/g.test(selector)) {
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (isHoverSelector) {
|
|
85
|
-
traverseParentRules(parent);
|
|
86
|
-
|
|
87
|
-
if (!isWrappedInHoverAtRule) {
|
|
88
|
-
stylelint.utils.report({
|
|
89
|
-
message: ruleMessages.accidentalHover(),
|
|
90
|
-
node: decl.parent,
|
|
91
|
-
result,
|
|
92
|
-
ruleName,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
31
|
+
accidentalHover({ decl, result });
|
|
96
32
|
}
|
|
97
33
|
|
|
98
34
|
/* BACKGROUND REPEAT */
|
|
99
35
|
if (options?.['background-repeat']) {
|
|
100
|
-
|
|
101
|
-
backgroundRepeatProps.hasBackgroundImage = true;
|
|
102
|
-
backgroundRepeatProps.isMissingBackgroundRepeat =
|
|
103
|
-
!findShorthandBackgroundRepeat(decl.value);
|
|
104
|
-
backgroundRepeatProps.nodeToReport = decl;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (decl.prop === 'background-image' && decl.value.includes('url(')) {
|
|
108
|
-
backgroundRepeatProps.hasBackgroundImage = true;
|
|
109
|
-
backgroundRepeatProps.nodeToReport = decl;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (decl.prop === 'background-repeat') {
|
|
113
|
-
backgroundRepeatProps.isMissingBackgroundRepeat = false;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (isLastStyleDeclaration) {
|
|
117
|
-
if (Object.values(backgroundRepeatProps).every((prop) => prop)) {
|
|
118
|
-
stylelint.utils.report({
|
|
119
|
-
message: ruleMessages.backgroundRepeat(),
|
|
120
|
-
node: backgroundRepeatProps.nodeToReport,
|
|
121
|
-
result,
|
|
122
|
-
ruleName,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
backgroundRepeatProps = { ...defaultBackgroundRepeatProps };
|
|
127
|
-
}
|
|
36
|
+
backgroundRepeat({ decl, isLastStyleDeclaration, result });
|
|
128
37
|
}
|
|
129
38
|
|
|
130
39
|
/* CUSTOM PROPERTY FALLBACKS */
|
|
131
40
|
if (options?.['custom-property-fallbacks']) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (propertiesWithoutFallback.length) {
|
|
135
|
-
if (Array.isArray(options?.['custom-property-fallbacks'])) {
|
|
136
|
-
if (options['custom-property-fallbacks'][0]) {
|
|
137
|
-
const patterns = options['custom-property-fallbacks'][1].ignore;
|
|
138
|
-
const patternMatched = propertiesWithoutFallback.some(
|
|
139
|
-
(property) => {
|
|
140
|
-
return patterns.some((pattern) =>
|
|
141
|
-
typeof pattern === 'string'
|
|
142
|
-
? new RegExp(pattern).test(property)
|
|
143
|
-
: pattern.test(property),
|
|
144
|
-
);
|
|
145
|
-
},
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
if (patternMatched) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
} else {
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
stylelint.utils.report({
|
|
157
|
-
message: ruleMessages.customPropertyFallbacks(),
|
|
158
|
-
node: decl,
|
|
159
|
-
result,
|
|
160
|
-
ruleName,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
41
|
+
customPropertyFallbacks({ decl, options, result });
|
|
163
42
|
}
|
|
164
43
|
|
|
165
44
|
/* FLEX WRAPPING */
|
|
166
45
|
if (options?.['flex-wrapping']) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
flexWrappingProps.nodeToReport = decl;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (decl.prop === 'flex-flow' && decl.value.includes('column')) {
|
|
173
|
-
flexWrappingProps.isFlexRow = false;
|
|
174
|
-
flexWrappingProps.isMissingFlexWrap = false;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (decl.prop === 'flex-direction' && decl.value.includes('column')) {
|
|
178
|
-
flexWrappingProps.isFlexRow = false;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
decl.prop === 'flex-wrap' ||
|
|
183
|
-
(decl.prop === 'flex-flow' && decl.value.includes('wrap'))
|
|
184
|
-
) {
|
|
185
|
-
flexWrappingProps.isMissingFlexWrap = false;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (isLastStyleDeclaration) {
|
|
189
|
-
if (Object.values(flexWrappingProps).every((prop) => prop)) {
|
|
190
|
-
stylelint.utils.report({
|
|
191
|
-
message: ruleMessages.flexWrapping(),
|
|
192
|
-
node: flexWrappingProps.nodeToReport,
|
|
193
|
-
result,
|
|
194
|
-
ruleName,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
46
|
+
flexWrapping({ decl, isLastStyleDeclaration, result });
|
|
47
|
+
}
|
|
197
48
|
|
|
198
|
-
|
|
199
|
-
|
|
49
|
+
/* GRID LINE NAMES */
|
|
50
|
+
if (options?.['grid-line-names']) {
|
|
51
|
+
gridLineNames({ decl, options, result });
|
|
200
52
|
}
|
|
201
53
|
|
|
202
54
|
/* SCROLL CHAINING */
|
|
203
55
|
if (options?.['scroll-chaining']) {
|
|
204
|
-
|
|
205
|
-
overflowProperties.includes(decl.prop) &&
|
|
206
|
-
(decl.value.includes('auto') || decl.value.includes('scroll'))
|
|
207
|
-
) {
|
|
208
|
-
scrollChainingProps.hasOverflow = true;
|
|
209
|
-
scrollChainingProps.nodeToReport = decl;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (decl.prop.includes('overscroll-behavior')) {
|
|
213
|
-
scrollChainingProps.hasOverscrollBehavior = true;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (isLastStyleDeclaration) {
|
|
217
|
-
if (
|
|
218
|
-
scrollChainingProps.hasOverflow &&
|
|
219
|
-
!scrollChainingProps.hasOverscrollBehavior
|
|
220
|
-
) {
|
|
221
|
-
stylelint.utils.report({
|
|
222
|
-
message: ruleMessages.scrollChaining(),
|
|
223
|
-
node: scrollChainingProps.nodeToReport,
|
|
224
|
-
result,
|
|
225
|
-
ruleName,
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
scrollChainingProps = { ...defaultScrollChainingProps };
|
|
230
|
-
}
|
|
56
|
+
scrollChaining({ decl, isLastStyleDeclaration, result });
|
|
231
57
|
}
|
|
232
58
|
|
|
233
59
|
/* SCROLLBAR GUTTER */
|
|
234
60
|
if (options?.['scrollbar-gutter']) {
|
|
235
|
-
|
|
236
|
-
overflowProperties.includes(decl.prop) &&
|
|
237
|
-
(decl.value.includes('auto') || decl.value.includes('scroll'))
|
|
238
|
-
) {
|
|
239
|
-
scrollbarGutterProps.hasOverflow = true;
|
|
240
|
-
scrollbarGutterProps.nodeToReport = decl;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (decl.prop.includes('scrollbar-gutter')) {
|
|
244
|
-
scrollbarGutterProps.hasScrollbarGutter = true;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (isLastStyleDeclaration) {
|
|
248
|
-
if (
|
|
249
|
-
scrollbarGutterProps.hasOverflow &&
|
|
250
|
-
!scrollbarGutterProps.hasScrollbarGutter
|
|
251
|
-
) {
|
|
252
|
-
stylelint.utils.report({
|
|
253
|
-
message: ruleMessages.scrollbarGutter(),
|
|
254
|
-
node: scrollbarGutterProps.nodeToReport,
|
|
255
|
-
result,
|
|
256
|
-
ruleName,
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
scrollbarGutterProps = { ...defaultScrollbarGutterProps };
|
|
261
|
-
}
|
|
61
|
+
scrollbarGutter({ decl, isLastStyleDeclaration, result });
|
|
262
62
|
}
|
|
263
63
|
|
|
264
64
|
/* VENDOR PREFIX GROUPING */
|
|
265
65
|
if (options?.['vendor-prefix-grouping']) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (hasMultiplePrefixes) {
|
|
269
|
-
stylelint.utils.report({
|
|
270
|
-
message: ruleMessages.vendorPrefixWGrouping(),
|
|
271
|
-
node: decl.parent,
|
|
272
|
-
result,
|
|
273
|
-
ruleName,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
66
|
+
vendorPrefixGrouping({ decl, result });
|
|
276
67
|
}
|
|
277
68
|
|
|
278
69
|
return;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import stylelint from 'stylelint';
|
|
2
|
+
import { ruleMessages, ruleName } from '../base.js';
|
|
3
|
+
import { overflowProperties } from '../../../utils/overflow.js';
|
|
4
|
+
|
|
5
|
+
const defaultScrollChainingProps = {
|
|
6
|
+
hasOverflow: false,
|
|
7
|
+
hasOverscrollBehavior: false,
|
|
8
|
+
nodeToReport: undefined,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
let scrollChainingProps = { ...defaultScrollChainingProps };
|
|
12
|
+
|
|
13
|
+
export function scrollChaining({ decl, isLastStyleDeclaration, result }) {
|
|
14
|
+
if (
|
|
15
|
+
overflowProperties.includes(decl.prop) &&
|
|
16
|
+
(decl.value.includes('auto') || decl.value.includes('scroll'))
|
|
17
|
+
) {
|
|
18
|
+
scrollChainingProps.hasOverflow = true;
|
|
19
|
+
scrollChainingProps.nodeToReport = decl;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (decl.prop.includes('overscroll-behavior')) {
|
|
23
|
+
scrollChainingProps.hasOverscrollBehavior = true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (isLastStyleDeclaration) {
|
|
27
|
+
if (
|
|
28
|
+
scrollChainingProps.hasOverflow &&
|
|
29
|
+
!scrollChainingProps.hasOverscrollBehavior
|
|
30
|
+
) {
|
|
31
|
+
stylelint.utils.report({
|
|
32
|
+
message: ruleMessages.scrollChaining(),
|
|
33
|
+
node: scrollChainingProps.nodeToReport,
|
|
34
|
+
result,
|
|
35
|
+
ruleName,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
scrollChainingProps = { ...defaultScrollChainingProps };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import stylelint from 'stylelint';
|
|
2
|
+
import { ruleMessages, ruleName } from '../base.js';
|
|
3
|
+
import { overflowProperties } from '../../../utils/overflow.js';
|
|
4
|
+
|
|
5
|
+
const defaultScrollbarGutterProps = {
|
|
6
|
+
hasOverflow: false,
|
|
7
|
+
hasScrollbarGutter: false,
|
|
8
|
+
nodeToReport: undefined,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
let scrollbarGutterProps = { ...defaultScrollbarGutterProps };
|
|
12
|
+
|
|
13
|
+
export function scrollbarGutter({ decl, isLastStyleDeclaration, result }) {
|
|
14
|
+
if (
|
|
15
|
+
overflowProperties.includes(decl.prop) &&
|
|
16
|
+
(decl.value.includes('auto') || decl.value.includes('scroll'))
|
|
17
|
+
) {
|
|
18
|
+
scrollbarGutterProps.hasOverflow = true;
|
|
19
|
+
scrollbarGutterProps.nodeToReport = decl;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (decl.prop.includes('scrollbar-gutter')) {
|
|
23
|
+
scrollbarGutterProps.hasScrollbarGutter = true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (isLastStyleDeclaration) {
|
|
27
|
+
if (
|
|
28
|
+
scrollbarGutterProps.hasOverflow &&
|
|
29
|
+
!scrollbarGutterProps.hasScrollbarGutter
|
|
30
|
+
) {
|
|
31
|
+
stylelint.utils.report({
|
|
32
|
+
message: ruleMessages.scrollbarGutter(),
|
|
33
|
+
node: scrollbarGutterProps.nodeToReport,
|
|
34
|
+
result,
|
|
35
|
+
ruleName,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
scrollbarGutterProps = { ...defaultScrollbarGutterProps };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import stylelint from 'stylelint';
|
|
2
|
+
import { ruleMessages, ruleName } from '../base.js';
|
|
3
|
+
import { findVendorPrefixes } from './utils.js';
|
|
4
|
+
|
|
5
|
+
export function vendorPrefixGrouping({ decl, result }) {
|
|
6
|
+
const hasMultiplePrefixes = findVendorPrefixes(decl.parent.selector);
|
|
7
|
+
|
|
8
|
+
if (hasMultiplePrefixes) {
|
|
9
|
+
stylelint.utils.report({
|
|
10
|
+
message: ruleMessages.vendorPrefixWGrouping(),
|
|
11
|
+
node: decl.parent,
|
|
12
|
+
result,
|
|
13
|
+
ruleName,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
File without changes
|
/package/src/{utils/findVendorPrefixes.js → rules/use-defensive-css/vendor-prefix-grouping/utils.js}
RENAMED
|
File without changes
|