├── .gitignore ├── LICENSE ├── README.md ├── __tests__ ├── ecss-align-display.test.js ├── ecss-class-child-prefix.test.js ├── ecss-class-combined-prefix.test.js ├── ecss-class-numbered.test.js ├── ecss-commented-code.test.js ├── ecss-component-dimensions.test.js ├── ecss-component-outside.test.js ├── ecss-content-block.test.js ├── ecss-content-float.test.js ├── ecss-content-margin.test.js ├── ecss-content-padding.test.js ├── ecss-flex-children.test.js ├── ecss-flex-prop.test.js ├── ecss-flex-shorthand.test.js ├── ecss-ignored-properties.test.js ├── ecss-large-selector-rule.test.js ├── ecss-not-class.test.js ├── ecss-overflow-hidden.test.js ├── ecss-padding-constraints.test.js ├── ecss-position-prop.test.js ├── ecss-position-sensitive.test.js ├── ecss-pseudo-disallowed.disabled.js ├── ecss-relative-width.test.js ├── ecss-selector-dimensions.test.js ├── ecss-selector-filename.test.js ├── ecss-selector-unnecessary.test.js ├── ecss-spacing-large.test.js ├── ecss-tag-scoped-class.test.js ├── ecss-technique-centered.test.js ├── ecss-width-100p.test.js ├── ecss-z-index-static.test.js └── fixtures │ ├── ecss-align-display.fail.css │ ├── ecss-align-display.pass.css │ ├── ecss-class-child-prefix.fail.css │ ├── ecss-class-child-prefix.pass.css │ ├── ecss-class-combined-prefix.fail.css │ ├── ecss-class-combined-prefix.pass.css │ ├── ecss-class-numbered.fail.css │ ├── ecss-class-numbered.pass.css │ ├── ecss-commented-code.fail.css │ ├── ecss-commented-code.pass.css │ ├── ecss-component-dimensions.fail.css │ ├── ecss-component-dimensions.pass.css │ ├── ecss-component-outside.fail.css │ ├── ecss-component-outside.pass.css │ ├── ecss-content-block.fail.css │ ├── ecss-content-block.pass.css │ ├── ecss-content-float.fail.css │ ├── ecss-content-float.pass.css │ ├── ecss-content-margin.fail.css │ ├── ecss-content-margin.pass.css │ ├── ecss-content-padding.fail.css │ ├── ecss-content-padding.pass.css │ ├── ecss-filename.fail.css │ ├── ecss-filename.pass.css │ ├── ecss-flex-children.fail.css │ ├── ecss-flex-children.pass.css │ ├── ecss-flex-prop.fail.css │ ├── ecss-flex-prop.pass.css │ ├── ecss-flex-shorthand.fail.css │ ├── ecss-flex-shorthand.pass.css │ ├── ecss-ignored-properties.fail.css │ ├── ecss-ignored-properties.pass.css │ ├── ecss-large-selector-rule.fail.css │ ├── ecss-large-selector-rule.pass.css │ ├── ecss-not-class.fail.css │ ├── ecss-not-class.pass.css │ ├── ecss-overflow-hidden.fail.css │ ├── ecss-overflow-hidden.pass.css │ ├── ecss-padding-constraints.fail.css │ ├── ecss-padding-constraints.pass.css │ ├── ecss-position-prop.fail.css │ ├── ecss-position-prop.pass.css │ ├── ecss-position-sensitive.fail.css │ ├── ecss-position-sensitive.pass.css │ ├── ecss-pseudo-disallowed.fail.css │ ├── ecss-pseudo-disallowed.pass.css │ ├── ecss-relative-width.fail.css │ ├── ecss-relative-width.pass.css │ ├── ecss-selector-dimensions.fail.css │ ├── ecss-selector-dimensions.pass.css │ ├── ecss-selector-filename.fail.css │ ├── ecss-selector-filename.pass.css │ ├── ecss-selector-unnecessary.fail.css │ ├── ecss-selector-unnecessary.pass.css │ ├── ecss-spacing-large.fail.css │ ├── ecss-spacing-large.pass.css │ ├── ecss-tag-scoped-class.fail.css │ ├── ecss-tag-scoped-class.pass.css │ ├── ecss-technique-centered.fail.css │ ├── ecss-technique-centered.pass.css │ ├── ecss-width-100p.fail.css │ ├── ecss-width-100p.pass.css │ ├── ecss-z-index-static.fail.css │ └── ecss-z-index-static.pass.css ├── index.js ├── jest.config.js ├── lib ├── chosenLang.js ├── configLang.js ├── messages.js ├── printUrl.js ├── printmessage.js └── selectors.js ├── package-lock.json ├── package.json └── plugins ├── ecss-align-display.js ├── ecss-class-child-prefix.js ├── ecss-class-combined-prefix.js ├── ecss-class-numbered.js ├── ecss-commented-code.js ├── ecss-component-dimensions.js ├── ecss-component-outside.js ├── ecss-content-block.js ├── ecss-content-float.js ├── ecss-content-margin.js ├── ecss-content-padding.js ├── ecss-flex-children.js ├── ecss-flex-prop.js ├── ecss-flex-shorthand.js ├── ecss-ignored-properties.js ├── ecss-large-selector-rule.js ├── ecss-not-class.js ├── ecss-overflow-hidden.js ├── ecss-padding-constraints.js ├── ecss-position-prop.js ├── ecss-position-sensitive.js ├── ecss-pseudo-disallowed.js ├── ecss-relative-width.js ├── ecss-selector-dimensions.js ├── ecss-selector-filename.js ├── ecss-selector-unnecessary.js ├── ecss-spacing-large.js ├── ecss-stylelint.config.js ├── ecss-tag-scoped-class.js ├── ecss-technique-centered.js ├── ecss-width-100p.js ├── ecss-z-index-static.js ├── stylelint-csstree-validator.js ├── stylelint-declaration-block-conjoined-properties.js ├── stylelint-magic-numbers.js ├── stylelint-z-index-value-constraint.js └── utils ├── hasPropertyValueInContext.js ├── isKeyframeSelector.js ├── matchesStringOrRegExp.js ├── optionsMatches.js ├── syntax-extension ├── index.js ├── less │ ├── LessEscaping.js │ ├── LessNamespace.js │ ├── LessVariable.js │ ├── LessVariableReference.js │ └── index.js └── sass │ ├── SassInterpolation.js │ ├── SassNamespace.js │ ├── SassVariable.js │ └── index.js ├── validateTypes.js └── vendorPrefixes.js /.gitignore: -------------------------------------------------------------------------------- 1 | tests 2 | tags 3 | commentaires.txt 4 | notes.txt 5 | .stylelintrc.json 6 | *.tgz 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023-2024 Marc-André Charpentier 4 | Copyright (c) 2020 Julian Strecker 5 | Copyright (c) 2016-2021 by Roman Dvornov 6 | Copyright (c) 2018 Krister Kari 7 | Copyright (c) 2015 - present Maxime Thirouin, David Clark & Richard Hallows 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stylelint Config for ECSS 2 | 3 | > Linting rules for writing Efficient CSS. 4 | 5 | **ECSS sets simple rules for efficient styling.** No more naming everything, no more technological dependencies. Only intentional, consistent, expressive, predictable, sustainable CSS. 6 | 7 | This config also adds a more detailed messaging system. 8 | 9 | For the complete documentation, see [ecss.info](https://ecss.info). 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm install @efficientcss/stylelint-config-ecss 15 | ``` 16 | 17 | ## Usage 18 | 19 | Add this package to the `extend`{.json} array in your Stylelint configuration or copy the following code in a new `.stylelintrc.json` file. 20 | 21 | ```json 22 | { 23 | "extends": ["@efficientcss/stylelint-config-ecss"] 24 | } 25 | ``` 26 | 27 | You can disable ECSS rules by using the Stylelint `overrides` array. The full list of rules is accessible in `index.js`. 28 | 29 | ```json 30 | { 31 | "extends": ["@efficientcss/stylelint-config-ecss"], 32 | "overrides": [ 33 | { 34 | "files": ["*"], 35 | "rules": { 36 | "ecss/large-selector-rule": null, 37 | "ecss/component-dimensions": null 38 | } 39 | } 40 | ] 41 | } 42 | ``` 43 | 44 | ## Notes 45 | 46 | You can opt out of selector naming check by adding a digit (ie: 1.base.css) or "x-" prefix (ie: x-quarantine.css). 47 | 48 | For further usage instructions, please refer to the [Stylelint official documentation](https://stylelint.io). 49 | -------------------------------------------------------------------------------- /__tests__/ecss-align-display.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-align-display.js"), 10 | rules: { 11 | "ecss/align-display": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-align-display.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-align-display.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(5); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-class-child-prefix.test.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url'; 2 | import stylelint from "stylelint"; 3 | import path from "path"; 4 | 5 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 6 | 7 | const config = { 8 | plugins: path.resolve(__dirname, "../plugins/ecss-class-child-prefix.js"), 9 | rules: { 10 | "ecss/class-child-prefix": true, 11 | }, 12 | }; 13 | 14 | describe("should pass", () => { 15 | it("should pass when CSS does not contain forbidden rules", async () => { 16 | const result = await stylelint.lint({ 17 | files: path.resolve(__dirname, "fixtures/ecss-class-child-prefix.pass.css"), 18 | config, 19 | }); 20 | expect(result.errored).toBe(false); 21 | }); 22 | }); 23 | 24 | describe("should fail", () => { 25 | it("should fail when CSS contains forbidden rules", async () => { 26 | const result = await stylelint.lint({ 27 | files: path.resolve(__dirname, "fixtures/ecss-class-child-prefix.fail.css"), 28 | config, 29 | }); 30 | expect(result.errored).toBe(true); 31 | expect(result.results[0].warnings).toHaveLength(5); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /__tests__/ecss-class-combined-prefix.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-class-combined-prefix.js"), 10 | rules: { 11 | "ecss/class-combined-prefix": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-class-combined-prefix.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-class-combined-prefix.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(5); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-class-numbered.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-class-numbered.js"), 10 | rules: { 11 | "ecss/class-numbered": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-class-numbered.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-class-numbered.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(4); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-commented-code.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-commented-code.js"), 10 | rules: { 11 | "ecss/commented-code": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-commented-code.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-commented-code.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(2); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-component-dimensions.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-component-dimensions.js"), 10 | rules: { 11 | "ecss/component-dimensions": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-component-dimensions.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-component-dimensions.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(2); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-component-outside.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-component-outside.js"), 10 | rules: { 11 | "ecss/component-outside": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-component-outside.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-component-outside.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(1); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-content-block.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-content-block.js"), 10 | rules: { 11 | "ecss/content-block": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-content-block.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-content-block.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(3); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-content-float.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-content-float.js"), 10 | rules: { 11 | "ecss/content-float": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-content-float.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-content-float.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(3); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-content-margin.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-content-margin.js"), 10 | rules: { 11 | "ecss/content-margin": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-content-margin.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-content-margin.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(4); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-content-padding.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-content-padding.js"), 10 | rules: { 11 | "ecss/content-padding": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-content-padding.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-content-padding.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(3); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-flex-children.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-flex-children.js"), 10 | rules: { 11 | "ecss/flex-children": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-flex-children.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-flex-children.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(3); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-flex-prop.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-flex-prop.js"), 10 | rules: { 11 | "ecss/flex-prop": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-flex-prop.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-flex-prop.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(4); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-flex-shorthand.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-flex-shorthand.js"), 10 | rules: { 11 | "ecss/flex-shorthand": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-flex-shorthand.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-flex-shorthand.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(1); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-ignored-properties.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-ignored-properties.js"), 10 | rules: { 11 | "ecss/ignored-properties": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-ignored-properties.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-ignored-properties.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(2); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-large-selector-rule.test.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url'; 2 | import stylelint from "stylelint"; 3 | import path from "path"; 4 | 5 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 6 | 7 | const config = { 8 | plugins: path.resolve(__dirname, "../plugins/ecss-large-selector-rule.js"), 9 | rules: { 10 | "ecss/large-selector-rule": true, 11 | }, 12 | }; 13 | 14 | describe("should pass", () => { 15 | it("should pass when CSS does not contain forbidden rules", async () => { 16 | const result = await stylelint.lint({ 17 | files: path.resolve(__dirname, "fixtures/ecss-large-selector-rule.pass.css"), 18 | config, 19 | }); 20 | expect(result.errored).toBe(false); 21 | }); 22 | }); 23 | 24 | describe("should fail", () => { 25 | it("should fail when CSS contains forbidden rules", async () => { 26 | const result = await stylelint.lint({ 27 | files: path.resolve(__dirname, "fixtures/ecss-large-selector-rule.fail.css"), 28 | config, 29 | }); 30 | expect(result.errored).toBe(true); 31 | expect(result.results[0].warnings).toHaveLength(4); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /__tests__/ecss-not-class.test.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url'; 2 | import stylelint from "stylelint"; 3 | import path from "path"; 4 | 5 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 6 | 7 | const config = { 8 | plugins: path.resolve(__dirname, "../plugins/ecss-not-class.js"), 9 | rules: { 10 | "ecss/not-class": true, 11 | }, 12 | }; 13 | 14 | describe("should pass", () => { 15 | it("should pass when CSS does not contain forbidden rules", async () => { 16 | const result = await stylelint.lint({ 17 | files: path.resolve(__dirname, "fixtures/ecss-not-class.pass.css"), 18 | config, 19 | }); 20 | expect(result.errored).toBe(false); 21 | }); 22 | }); 23 | 24 | describe("should fail", () => { 25 | it("should fail when CSS contains forbidden rules", async () => { 26 | const result = await stylelint.lint({ 27 | files: path.resolve(__dirname, "fixtures/ecss-not-class.fail.css"), 28 | config, 29 | }); 30 | expect(result.errored).toBe(true); 31 | expect(result.results[0].warnings).toHaveLength(3); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /__tests__/ecss-overflow-hidden.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-overflow-hidden.js"), 10 | rules: { 11 | "ecss/overflow-hidden": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-overflow-hidden.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-overflow-hidden.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(5); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-padding-constraints.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-padding-constraints.js"), 10 | rules: { 11 | "ecss/padding-constraints": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-padding-constraints.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-padding-constraints.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(6); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-position-prop.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-position-prop.js"), 10 | rules: { 11 | "ecss/position-prop": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-position-prop.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-position-prop.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(4); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-position-sensitive.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-position-sensitive.js"), 10 | rules: { 11 | "ecss/position-sensitive": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-position-sensitive.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-position-sensitive.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(2); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-pseudo-disallowed.disabled.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-pseudo-disallowed.js"), 10 | rules: { 11 | "ecss/pseudo-disallowed": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-pseudo-disallowed.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-pseudo-disallowed.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(1); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-relative-width.test.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url'; 2 | import stylelint from "stylelint"; 3 | import path from "path"; 4 | 5 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 6 | 7 | const config = { 8 | plugins: path.resolve(__dirname, "../plugins/ecss-relative-width.js"), 9 | rules: { 10 | "ecss/relative-width": true, 11 | }, 12 | }; 13 | 14 | describe("should pass", () => { 15 | it("should pass when CSS does not contain forbidden rules", async () => { 16 | const result = await stylelint.lint({ 17 | files: path.resolve(__dirname, "fixtures/ecss-relative-width.pass.css"), 18 | config, 19 | }); 20 | expect(result.errored).toBe(false); 21 | }); 22 | }); 23 | 24 | describe("should fail", () => { 25 | it("should fail when CSS contains forbidden rules", async () => { 26 | const result = await stylelint.lint({ 27 | files: path.resolve(__dirname, "fixtures/ecss-relative-width.fail.css"), 28 | config, 29 | }); 30 | expect(result.errored).toBe(true); 31 | expect(result.results[0].warnings).toHaveLength(1); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /__tests__/ecss-selector-dimensions.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-selector-dimensions.js"), 10 | rules: { 11 | "ecss/selector-dimensions": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-selector-dimensions.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-selector-dimensions.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(8); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-selector-filename.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-selector-filename.js"), 10 | rules: { 11 | "ecss/selector-filename": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-selector-filename.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-selector-filename.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(4); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-selector-unnecessary.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-selector-unnecessary.js"), 10 | rules: { 11 | "ecss/selector-unnecessary": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-selector-unnecessary.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-selector-unnecessary.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(4); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-spacing-large.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-spacing-large.js"), 10 | rules: { 11 | "ecss/spacing-large": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-spacing-large.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-spacing-large.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(4); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-tag-scoped-class.test.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url'; 2 | import stylelint from "stylelint"; 3 | import path from "path"; 4 | 5 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 6 | 7 | const config = { 8 | plugins: path.resolve(__dirname, "../plugins/ecss-tag-scoped-class.js"), 9 | rules: { 10 | "ecss/tag-scoped-class": true, 11 | }, 12 | }; 13 | 14 | describe("should pass", () => { 15 | it("should pass when CSS does not contain forbidden rules", async () => { 16 | const result = await stylelint.lint({ 17 | files: path.resolve(__dirname, "fixtures/ecss-tag-scoped-class.pass.css"), 18 | config, 19 | }); 20 | expect(result.errored).toBe(false); 21 | }); 22 | }); 23 | 24 | describe("should fail", () => { 25 | it("should fail when CSS contains forbidden rules", async () => { 26 | const result = await stylelint.lint({ 27 | files: path.resolve(__dirname, "fixtures/ecss-tag-scoped-class.fail.css"), 28 | config, 29 | }); 30 | expect(result.errored).toBe(true); 31 | expect(result.results[0].warnings).toHaveLength(2); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /__tests__/ecss-technique-centered.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-technique-centered.js"), 10 | rules: { 11 | "ecss/technique-centered": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-technique-centered.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-technique-centered.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(1); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/ecss-width-100p.test.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url'; 2 | import stylelint from "stylelint"; 3 | import path from "path"; 4 | 5 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 6 | 7 | const config = { 8 | plugins: path.resolve(__dirname, "../plugins/ecss-width-100p.js"), 9 | rules: { 10 | "ecss/width-100p": true, 11 | }, 12 | }; 13 | 14 | describe("should pass", () => { 15 | it("should pass when CSS does not contain forbidden rules", async () => { 16 | const result = await stylelint.lint({ 17 | files: path.resolve(__dirname, "fixtures/ecss-width-100p.pass.css"), 18 | config, 19 | }); 20 | expect(result.errored).toBe(false); 21 | }); 22 | }); 23 | 24 | describe("should fail", () => { 25 | it("should fail when CSS contains forbidden rules", async () => { 26 | const result = await stylelint.lint({ 27 | files: path.resolve(__dirname, "fixtures/ecss-width-100p.fail.css"), 28 | config, 29 | }); 30 | expect(result.errored).toBe(true); 31 | expect(result.results[0].warnings).toHaveLength(2); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /__tests__/ecss-z-index-static.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { fileURLToPath } from 'node:url'; 3 | import stylelint from "stylelint"; 4 | import path from "path"; 5 | 6 | const __dirname = fileURLToPath(new URL('.', import.meta.url)); 7 | 8 | const config = { 9 | plugins: path.resolve(__dirname, "../plugins/ecss-z-index-static.js"), 10 | rules: { 11 | "ecss/z-index-static": true, 12 | }, 13 | }; 14 | 15 | describe("should pass", () => { 16 | it("should pass when CSS does not contain forbidden rules", async () => { 17 | const result = await stylelint.lint({ 18 | files: path.resolve(__dirname, "fixtures/ecss-z-index-static.pass.css"), 19 | config, 20 | }); 21 | expect(result.errored).toBe(false); 22 | }); 23 | }); 24 | 25 | describe("should fail", () => { 26 | it("should fail when CSS contains forbidden rules", async () => { 27 | const result = await stylelint.lint({ 28 | files: path.resolve(__dirname, "fixtures/ecss-z-index-static.fail.css"), 29 | config, 30 | }); 31 | expect(result.errored).toBe(true); 32 | expect(result.results[0].warnings).toHaveLength(4); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-align-display.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-align-display { 2 | align-items: center; 3 | 4 | & .__child-error { 5 | align-items: center; 6 | } 7 | 8 | } 9 | 10 | .ecss-align-display { 11 | gap: 20px; 12 | } 13 | 14 | .ecss-align-display { 15 | justify-content: center; 16 | 17 | &.is-variant-error { 18 | align-items: baseline; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-align-display.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-align-display { 2 | display: flex; 3 | align-items: center; 4 | 5 | & .__child-error { 6 | display: flex; 7 | align-items: center; 8 | } 9 | 10 | } 11 | 12 | .ecss-align-display { 13 | display: flex; 14 | gap: 20px; 15 | } 16 | 17 | .ecss-align-display { 18 | display: flex; 19 | justify-content: center; 20 | 21 | &.is-variant-error { 22 | align-items: baseline; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-class-child-prefix.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-class-child-prefix { 2 | & .child { 3 | display: block; 4 | } 5 | 6 | & .variant { 7 | display: block; 8 | } 9 | } 10 | 11 | .ecss-class-child-prefix .child-fail { 12 | display: block; 13 | } 14 | 15 | [id=ecss-class-child-prefix] .child-fail { 16 | position: relative; 17 | } 18 | 19 | .ecss-class-child-prefix .child-fail, 20 | .ecss-class-child-prefix .__child-pass { 21 | display: block; 22 | } 23 | 24 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-class-child-prefix.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-class-child-prefix { 2 | & .__child { 3 | position: relative; 4 | } 5 | 6 | & .is-variant { 7 | display: block; 8 | } 9 | 10 | & .as-grid { 11 | display: grid; 12 | } 13 | 14 | & .to-left { 15 | position: relative; 16 | } 17 | 18 | & .for-menu-toggling { 19 | position: sticky; 20 | } 21 | 22 | & .on-hover:hover { 23 | color: aqua; 24 | } 25 | } 26 | 27 | :is(.ecss-class-child-prefix, .ecss-class-child-prefix) { 28 | display: block; 29 | } 30 | 31 | .ecss-class-child-prefix { 32 | display: block; 33 | } 34 | 35 | .ecss-class-child-prefix .__child { 36 | display: block; 37 | } 38 | 39 | .ecss-class-child-prefix, .ecss-class-child-prefix-two { 40 | display: block; 41 | } 42 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-class-combined-prefix.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-class-combined-prefix { 2 | &.variant { 3 | display: block; 4 | } 5 | 6 | &.grid { 7 | display: grid; 8 | } 9 | 10 | &.left { 11 | position: relative; 12 | } 13 | 14 | &.menu-toggling { 15 | position: sticky; 16 | } 17 | 18 | &.hover:hover { 19 | color: aqua; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-class-combined-prefix.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-class-combined-prefix { 2 | &.is-variant { 3 | display: block; 4 | } 5 | 6 | &.as-grid { 7 | display: grid; 8 | } 9 | 10 | &.to-left { 11 | position: relative; 12 | } 13 | 14 | &.for-menu-toggling { 15 | position: sticky; 16 | } 17 | 18 | &.on-hover:hover { 19 | color: aqua; 20 | } 21 | } 22 | 23 | .ecss-class-combined-prefix.is-variant { 24 | display: block; 25 | } 26 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-class-numbered.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-class-numbered-2 { 2 | display: block; 3 | 4 | &.is-variant-4 { 5 | display: block; 6 | } 7 | 8 | & .__bla-3 { 9 | color: blue; 10 | } 11 | } 12 | 13 | .ecss-class-numbered { 14 | &.is-variant-3 { 15 | display: block; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-class-numbered.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-class-numbered { 2 | display: block; 3 | 4 | &.as-col-4 { 5 | display: block; 6 | } 7 | 8 | & .__grid-3 { 9 | color: blue; 10 | } 11 | } 12 | 13 | .ecss-class-numbered { 14 | &.as-h2 { 15 | display: block; 16 | } 17 | } 18 | 19 | .ecss-class-numbered { 20 | & h3 { 21 | display: block; 22 | } 23 | } 24 | 25 | .ecss-class-numbered:nth-of-type(4) { 26 | display: block; 27 | } 28 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-commented-code.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-commented-code { 2 | position: relative; 3 | /* font-size: 20px; */ 4 | 5 | &.is-variant { 6 | /* background: red; */ 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-commented-code.pass.css: -------------------------------------------------------------------------------- 1 | /* Accepted real comments */ 2 | .ecss-commented-code { 3 | position: relative; 4 | } 5 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-component-dimensions.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-component-dimensions { 2 | width: 100px; 3 | height: 200px; 4 | } 5 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-component-dimensions.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-component-dimensions { 2 | min-width: 100px; 3 | min-height: 200px; 4 | 5 | & .__child { 6 | width: 100em; 7 | } 8 | } 9 | 10 | .ecss-component-dimensions.is-variant { 11 | max-width: 100px; 12 | } 13 | 14 | .ecss-component-dimensions { 15 | max-width: 300px; 16 | } 17 | 18 | .ecss-component-dimensions { 19 | max-width: 100px; 20 | max-height: 200px; 21 | } 22 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-component-outside.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-component-outside { 2 | margin: 20px; 3 | } 4 | 5 | .ecss-component-outside { 6 | &.is-variant { 7 | margin: 20px; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-component-outside.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-component-outside>*+* { 2 | margin: 20px; 3 | } 4 | 5 | .ecss-component-outside { 6 | &>*+* { 7 | margin: 20px; 8 | } 9 | } 10 | 11 | .ecss-component-outside { 12 | & .__child { 13 | margin-top: 20px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-content-block.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-content-block p { 2 | display: inline-flex; 3 | } 4 | 5 | .ecss-content-block { 6 | & h2 { 7 | display: flex; 8 | } 9 | } 10 | 11 | .ecss-content-block h1 { 12 | display: grid; 13 | } 14 | 15 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-content-block.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-content-block p { 2 | display: block; 3 | } 4 | 5 | .ecss-content-block { 6 | & h2 { 7 | display: block; 8 | } 9 | } 10 | 11 | .ecss-content-block { 12 | & p { 13 | display: inline; 14 | } 15 | } 16 | 17 | .ecss-content-block h3 { 18 | display: inline-block; 19 | } 20 | 21 | .ecss-content-block p { 22 | display: flex; 23 | 24 | &::before { 25 | flex-shrink: 1; 26 | } 27 | } 28 | 29 | .ecss-content-block h2 { 30 | display: inline-flex; 31 | 32 | &::before { 33 | flex-shrink: 1; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-content-float.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-content-float { 2 | float: left; 3 | } 4 | 5 | .ecss-content-float h2 { 6 | float: left; 7 | } 8 | 9 | .ecss-content-float { 10 | & .__child { 11 | float: right; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-content-float.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-content-float img { 2 | float: left; 3 | } 4 | 5 | .ecss-content-float figure { 6 | float: left; 7 | } 8 | 9 | .ecss-content-float { 10 | & figure { 11 | float: right; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-content-margin.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-content-margin p { 2 | margin: 20px; 3 | } 4 | 5 | .ecss-content-margin h2 { 6 | margin-right: 20px; 7 | } 8 | 9 | .ecss-content-margin { 10 | & h2 { 11 | margin-left: 20px; 12 | } 13 | } 14 | 15 | .ecss-content-margin { 16 | & h2 { 17 | margin-inline: 20px; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-content-margin.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-content-margin p { 2 | margin-top: 20px; 3 | } 4 | 5 | .ecss-content-margin h2 { 6 | margin-bottom: 20px; 7 | 8 | &>i { 9 | margin-left: 60px; 10 | } 11 | } 12 | 13 | .ecss-content-margin { 14 | & h2 { 15 | margin-bottom: 20px; 16 | } 17 | 18 | & p { 19 | margin-block: 30px; 20 | } 21 | 22 | & blockquote { 23 | margin-block-end: 30px; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-content-padding.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-content-padding p { 2 | background-color: red; 3 | padding: 20px; 4 | } 5 | 6 | .ecss-content-padding { 7 | & h2 { 8 | background-color: red; 9 | padding: 20px; 10 | } 11 | 12 | & p { 13 | padding-bottom: 20px; 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-content-padding.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-content-padding { 2 | background-color: red; 3 | padding: 20px; 4 | } 5 | 6 | .ecss-content-padding { 7 | & header { 8 | background-color: red; 9 | padding: 20px; 10 | } 11 | } 12 | 13 | .ecss-content-padding h2 i { 14 | background-color: inherit; 15 | padding: 10px; 16 | } 17 | 18 | .ecss-content-padding h2 { 19 | &>i { 20 | background-color: inherit; 21 | padding: 10px; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-filename.fail.css: -------------------------------------------------------------------------------- 1 | .not-filename { 2 | display: block; 3 | 4 | &.__input { 5 | display: block; 6 | } 7 | 8 | & div { 9 | display: block; 10 | } 11 | 12 | &.as-grid { 13 | display: grid; 14 | } 15 | } 16 | 17 | :is(.filename, .bla) { 18 | display: block; 19 | } 20 | 21 | :is(.not-filename, .filename) { 22 | display: block; 23 | } 24 | 25 | :where(.not-filename, .filename) { 26 | display: block; 27 | } 28 | 29 | :where(.bla, .not-filename, .filename) { 30 | display: block; 31 | } 32 | 33 | :where(.not-filename) { 34 | display: block; 35 | } 36 | 37 | :is(.not-filename) { 38 | display: block; 39 | } 40 | 41 | .not-filename { 42 | display: block; 43 | } 44 | 45 | .not-filename, .filename { 46 | display: block; 47 | } 48 | 49 | .filename, .not-filename { 50 | display: block; 51 | } 52 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-filename.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-filename { 2 | display: block; 3 | } 4 | 5 | .ecss-filename { 6 | display: block; 7 | 8 | &.__input { 9 | display: block; 10 | } 11 | 12 | & div { 13 | display: block; 14 | } 15 | 16 | &.as-grid { 17 | display: grid; 18 | } 19 | } 20 | 21 | :is(.ecss-filename, .ecss-filename-bla) { 22 | display: block; 23 | } 24 | 25 | :where(.ecss-filename, .ecss-filename-bla) { 26 | display: block; 27 | } 28 | 29 | :where(.ecss-filename) { 30 | display: block; 31 | } 32 | 33 | :is(.ecss-filename) { 34 | display: block; 35 | } 36 | 37 | @keyframes test { 38 | 0% { 39 | color: black; 40 | } 41 | 100% { 42 | color: white; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-flex-children.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-flex-children { 2 | & .__child { 3 | align-self: center; 4 | display: flex; 5 | } 6 | } 7 | 8 | .ecss-flex-children { 9 | display: block; 10 | 11 | & .__child { 12 | flex-grow: 1; 13 | } 14 | } 15 | 16 | .ecss-flex-children { 17 | flex-shrink: 0; 18 | } 19 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-flex-children.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-flex-children { 2 | display: flex; 3 | 4 | & .__child { 5 | align-self: center; 6 | } 7 | } 8 | 9 | .ecss-flex-children { 10 | display: inline-flex; 11 | 12 | & .__child { 13 | flex-grow: 1; 14 | } 15 | 16 | &::before { 17 | flex-grow: 1; 18 | } 19 | 20 | &:after { 21 | flex-shrink: 2; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-flex-prop.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-flex-prop { 2 | flex-direction: row-reverse; 3 | 4 | &.is-variant { 5 | flex-wrap: wrap; 6 | } 7 | } 8 | 9 | .ecss-flex-prop { 10 | &.is-variant { 11 | flex-flow: row wrap; 12 | } 13 | } 14 | 15 | .ecss-flex-prop { 16 | & .__child { 17 | flex-direction: column; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-flex-prop.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-flex-prop { 2 | display: flex; 3 | flex-direction: row-reverse; 4 | 5 | &.is-variant { 6 | flex-wrap: wrap; 7 | } 8 | } 9 | 10 | .ecss-flex-prop { 11 | &.is-variant { 12 | display: flex; 13 | flex-flow: row wrap; 14 | } 15 | } 16 | 17 | .ecss-flex-prop { 18 | & .__child { 19 | display: flex; 20 | flex-direction: column; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-flex-shorthand.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-flex-shorthand { 2 | display: flex; 3 | 4 | & .__child { 5 | flex: 10px 1 2; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-flex-shorthand.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-flex-shorthand { 2 | display: flex; 3 | 4 | & .__child { 5 | flex-grow: 1; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-ignored-properties.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-ignored-properties { 2 | display: inline; 3 | width: 50px; 4 | } 5 | 6 | .ecss-ignored-properties { 7 | position: static; 8 | top: 50px; 9 | } 10 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-ignored-properties.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-ignored-properties { 2 | display: block; 3 | min-width: 50px; 4 | } 5 | 6 | .ecss-ignored-properties { 7 | position: relative; 8 | top: 50px; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-large-selector-rule.fail.css: -------------------------------------------------------------------------------- 1 | article { 2 | background-color: red; 3 | } 4 | 5 | div { 6 | padding: 20px; 7 | } 8 | 9 | header { 10 | position: absolute; 11 | } 12 | 13 | footer { 14 | border-color: blue; 15 | } 16 | 17 | aside { 18 | & header { 19 | display: flex; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-large-selector-rule.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-large-selector { 2 | & article { 3 | background-color: red; 4 | } 5 | } 6 | 7 | .ecss-large-selector div { 8 | padding: 20px; 9 | } 10 | 11 | .ecss-large-selector { 12 | & header { 13 | position: absolute; 14 | } 15 | } 16 | 17 | .ecss-large-selector footer { 18 | border-color: blue; 19 | } 20 | 21 | article { 22 | .__child { 23 | display: block 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-not-class.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-not-class:not(.button) { 2 | display: inline; 3 | } 4 | 5 | 6 | .ecss-not-class { 7 | &:not(.container) { 8 | display: inline; 9 | } 10 | } 11 | 12 | .ecss-not-class:not([hover], .class) :where(.__conteneur) { 13 | display: block; 14 | } 15 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-not-class.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-not-class:not(a) { 2 | display: inline; 3 | } 4 | 5 | .ecss-not-class :not(span) { 6 | display: inline; 7 | } 8 | 9 | .ecss-not-class:not(:hover) .__conteneur { 10 | display: block; 11 | } 12 | 13 | .ecss-not-class:not([class]) { 14 | display: block; 15 | } 16 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-overflow-hidden.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-overflow-hidden { 2 | overflow: hidden; 3 | } 4 | 5 | .ecss-overflow-hidden { 6 | overflow: hidden; 7 | } 8 | 9 | .ecss-overflow-hidden { 10 | 11 | &.is-variant { 12 | overflow: hidden; 13 | } 14 | 15 | & .is-child { 16 | overflow: hidden; 17 | } 18 | } 19 | 20 | .ecss-overflow-hidden { 21 | aspect-ratio: 16/9; 22 | 23 | & .is-child { 24 | overflow: hidden; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-overflow-hidden.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-overflow-hidden { 2 | overflow: hidden; 3 | aspect-ratio: 16/9; 4 | } 5 | 6 | .ecss-overflow-hidden { 7 | overflow: auto; 8 | } 9 | 10 | .ecss-overflow-hidden { 11 | overflow: hidden; 12 | border-radius: 10px; 13 | } 14 | 15 | .ecss-overflow-hidden { 16 | border-radius: 10px; 17 | 18 | &.is-variant { 19 | overflow: hidden; 20 | } 21 | } 22 | 23 | .ecss-overflow-hidden picture { 24 | overflow: hidden; 25 | } 26 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-padding-constraints.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-padding-constraints { 2 | padding: 20px; 3 | } 4 | 5 | .ecss-padding-constraints { 6 | padding-top: 20px; 7 | } 8 | 9 | .ecss-padding-constraints { 10 | 11 | & .__child { 12 | padding: 20px; 13 | } 14 | } 15 | 16 | .ecss-padding-constraints { 17 | &.is-variant { 18 | padding: 10px; 19 | } 20 | } 21 | 22 | .ecss-padding-constraints div { 23 | padding-top: 10px; 24 | } 25 | 26 | 27 | .ecss-padding-constraints :is(p) { 28 | padding-top: 10px; 29 | } 30 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-padding-constraints.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-padding-constraints { 2 | padding: 20px; 3 | padding-top: 10px; 4 | background-color: black; 5 | } 6 | 7 | .ecss-padding-constraints { 8 | padding: 20px; 9 | border: 1px solid blue; 10 | } 11 | 12 | .ecss-padding-constraints { 13 | 14 | & .__child { 15 | border-radius: 20px; 16 | overflow: auto; 17 | padding-left: 20px; 18 | padding-right: 20px; 19 | } 20 | 21 | &.is-variant a { 22 | padding: 20px; 23 | } 24 | } 25 | 26 | .ecss-padding-constraints { 27 | background: blue; 28 | &.is-variant { 29 | padding: 10px; 30 | } 31 | } 32 | 33 | .ecss-padding-constraints a { 34 | padding-top: 10px; 35 | } 36 | 37 | 38 | .ecss-padding-constraints :is(ul, button, input) { 39 | padding: 20px; 40 | padding-top: 10px; 41 | } 42 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-position-prop.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-position-prop { 2 | top: 10px; 3 | 4 | &.is-variant { 5 | bottom: 10px; 6 | top: auto; 7 | } 8 | 9 | } 10 | 11 | .ecss-position-prop { 12 | position: relative; 13 | 14 | & .__child { 15 | right: 20px; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-position-prop.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-position-prop { 2 | position: relative; 3 | top: 10px; 4 | 5 | &.is-variant { 6 | bottom: 10px; 7 | top: auto; 8 | } 9 | 10 | & .__child { 11 | position: relative; 12 | right: 20px; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-position-sensitive.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-position-sensitive { 2 | position: absolute; 3 | 4 | &.is-variant { 5 | position: fixed; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-position-sensitive.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-position-sensitive { 2 | position: relative; 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-pseudo-disallowed.fail.css: -------------------------------------------------------------------------------- 1 | 2 | /* Invalid CSS for failing test */ 3 | .invalid-selector { 4 | color: red !important; /* This should trigger an error */ 5 | } 6 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-pseudo-disallowed.pass.css: -------------------------------------------------------------------------------- 1 | 2 | /* Valid CSS for passing test */ 3 | .valid-selector { 4 | color: blue; 5 | padding: 5px; 6 | } 7 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-relative-width.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-relative-width { 2 | display: flex; 3 | 4 | & .__child { 5 | width: 50%; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-relative-width.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-relative-width { 2 | display: flex; 3 | 4 | & .__child { 5 | flex-basis: 50%; 6 | } 7 | } 8 | 9 | .ecss-relative-width__img { 10 | width: 100%; 11 | } 12 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-selector-dimensions.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-selector-dimensions { 2 | height: 500px; 3 | max-height: 100vh; 4 | } 5 | 6 | .ecss-selector-dimensions { 7 | height: 100%; 8 | } 9 | 10 | .ecss-selector-dimensions { 11 | max-height: 500px; 12 | height: 500px; 13 | } 14 | 15 | .ecss-selector-dimensions { 16 | height: 500px; 17 | 18 | & .__child { 19 | height: 400px; 20 | } 21 | 22 | &.is-variant { 23 | max-height: 400px; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-selector-dimensions.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-selector-dimensions img { 2 | width: 500px; 3 | height: 500px; 4 | } 5 | 6 | .ecss-selector-dimensions input { 7 | width: 500px; 8 | } 9 | 10 | .ecss-selector-dimensions { 11 | overflow: auto; 12 | max-height: 100dvh; 13 | min-height: 50dvh; 14 | 15 | & .__logo { 16 | height: 500px; 17 | } 18 | 19 | &.is-variant img { 20 | height: 10rem; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-selector-filename.fail.css: -------------------------------------------------------------------------------- 1 | .not-ecss-selector-filename { 2 | display: block; 3 | } 4 | 5 | [id=not-ecss-selector-filename] { 6 | display: block; 7 | 8 | &.is-variant { 9 | color: red; 10 | } 11 | } 12 | 13 | :where(.not-ecss-selector-filename) { 14 | display: flex; 15 | } 16 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-selector-filename.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-selector-filename { 2 | display: block; 3 | } 4 | 5 | [id=ecss-selector-filename] { 6 | display: block; 7 | 8 | &.is-variant { 9 | color: red; 10 | } 11 | } 12 | 13 | :where(.ecss-selector-filename) { 14 | display: flex; 15 | } 16 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-selector-unnecessary.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-selector-unnecessary div h2 { 2 | margin-top: 20px; 3 | } 4 | 5 | .ecss-selector-unnecessary ul li { 6 | margin-top: 20px; 7 | } 8 | 9 | .ecss-selector-unnecessary { 10 | & div h2 { 11 | color: grey; 12 | } 13 | 14 | & footer p { 15 | font-size: 16px; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-selector-unnecessary.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-selector-unnecessary h2 { 2 | margin-top: 20px; 3 | } 4 | 5 | .ecss-selector-unnecessary header { 6 | background-color: blue; 7 | padding: 20px; 8 | } 9 | 10 | .ecss-selector-unnecessary { 11 | & h2 { 12 | color: grey; 13 | } 14 | 15 | & > ul > li { 16 | display: flex; 17 | } 18 | } 19 | 20 | .ecss-selector-unnecessary ~ ul li { 21 | display: block; 22 | } 23 | 24 | .ecss-selector-unnecessary-section a { 25 | color: #fcfcfc; 26 | } 27 | 28 | .ecss-selector-unnecessary-section ul { 29 | color: #fcfcfc; 30 | } 31 | 32 | .ecss-selector-unnecessary-footer ul { 33 | color: #fcfcfc; 34 | } 35 | 36 | 37 | .ecss-selector-unnecessary-footer > li a { 38 | color: #fcfcfc; 39 | } 40 | 41 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-spacing-large.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-spacing-large { 2 | margin: 260px; 3 | } 4 | 5 | .ecss-spacing-large { 6 | margin-left: 320px; 7 | } 8 | 9 | .ecss-spacing-large { 10 | margin-inline-end: 400px; 11 | } 12 | 13 | .ecss-spacing-large div { 14 | padding-inline-start: 300px; 15 | } 16 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-spacing-large.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-spacing-large { 2 | margin: 60px; 3 | } 4 | 5 | .ecss-spacing-large { 6 | margin-left: 20px; 7 | } 8 | 9 | .ecss-spacing-large { 10 | margin-top: 200px; 11 | } 12 | 13 | .ecss-spacing-large { 14 | margin-block: 200px; 15 | } 16 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-tag-scoped-class.fail.css: -------------------------------------------------------------------------------- 1 | nav .dropdown { 2 | position: absolute; 3 | } 4 | 5 | footer .ecss-tag-scoped-class { 6 | display: block; 7 | } 8 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-tag-scoped-class.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-tag-scoped-class .child { 2 | display: block; 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-technique-centered.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-technique-centered { 2 | transform: translate(-50%, -50%) 3 | } 4 | 5 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-technique-centered.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-technique-centered { 2 | display: flex; 3 | place-items: center; 4 | } 5 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-width-100p.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-width-100p { 2 | display: flex; 3 | width: 100%; 4 | 5 | & .__child { 6 | width: 100%; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-width-100p.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-width-100p { 2 | display: flex; 3 | 4 | & .__child { 5 | width: 50%; 6 | 7 | } 8 | } 9 | 10 | .ecss-width-100p__img { 11 | min-width: 100%; 12 | } 13 | 14 | .ecss-width-100p img { 15 | width: 100%; 16 | } 17 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-z-index-static.fail.css: -------------------------------------------------------------------------------- 1 | .ecss-z-index-static { 2 | z-index: 1; 3 | } 4 | 5 | .ecss-z-index-static { 6 | position: relative; 7 | 8 | & .__child { 9 | z-index: 1; 10 | } 11 | } 12 | 13 | .ecss-z-index-static { 14 | position: static; 15 | 16 | &.is-variant { 17 | z-index: 1; 18 | } 19 | 20 | & .__child { 21 | z-index: 1; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /__tests__/fixtures/ecss-z-index-static.pass.css: -------------------------------------------------------------------------------- 1 | .ecss-z-index-static { 2 | position: relative; 3 | z-index: 1; 4 | } 5 | 6 | .ecss-z-index-static { 7 | position: relative; 8 | 9 | &.is-variant { 10 | z-index: 1; 11 | } 12 | 13 | & .__child { 14 | position: absolute; 15 | z-index: 1; 16 | } 17 | } 18 | 19 | .ecss-z-index-static { 20 | &.is-variant { 21 | position: relative; 22 | z-index: 1; 23 | } 24 | 25 | & .__child { 26 | position: absolute; 27 | z-index: 1; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import printMessage from './lib/printmessage.js'; 2 | import * as selectors from './lib/selectors.js'; 3 | 4 | export default { 5 | "plugins": [ 6 | "./plugins/ecss-position-sensitive", 7 | "./plugins/ecss-align-display", 8 | "./plugins/ecss-z-index-static", 9 | "./plugins/ecss-overflow-hidden", 10 | "./plugins/ecss-large-selector-rule", 11 | "./plugins/ecss-content-block", 12 | "./plugins/ecss-class-numbered", 13 | "./plugins/ecss-not-class", 14 | "./plugins/ecss-selector-dimensions", 15 | "./plugins/ecss-relative-width", 16 | "./plugins/ecss-width-100p", 17 | "./plugins/ecss-flex-prop", 18 | "./plugins/ecss-flex-children", 19 | "./plugins/ecss-flex-shorthand", 20 | "./plugins/ecss-technique-centered", 21 | "./plugins/ecss-padding-constraints", 22 | "./plugins/ecss-component-outside", 23 | "./plugins/ecss-spacing-large", 24 | "./plugins/ecss-position-prop", 25 | "./plugins/ecss-tag-scoped-class", 26 | "./plugins/ecss-class-child-prefix", 27 | "./plugins/ecss-content-float", 28 | "./plugins/ecss-component-dimensions", 29 | "./plugins/ecss-content-margin", 30 | "./plugins/ecss-class-combined-prefix", 31 | "./plugins/ecss-content-padding", 32 | "./plugins/ecss-selector-filename", 33 | "./plugins/stylelint-z-index-value-constraint", 34 | "./plugins/stylelint-csstree-validator", 35 | "./plugins/ecss-ignored-properties", 36 | "./plugins/ecss-selector-unnecessary", 37 | "./plugins/stylelint-magic-numbers", 38 | "./plugins/ecss-commented-code", 39 | "stylelint-file-max-lines" 40 | ], 41 | "rules": { 42 | "ecss/position-sensitive": [true, { 43 | "severity": "warning", 44 | "message": (selector, prop) => { 45 | return printMessage("position-sensitive", selector, prop); 46 | } 47 | }], 48 | "ecss/align-display": [true, { 49 | "message": (selector, prop) => { 50 | return printMessage("align-display", selector, prop); 51 | } 52 | }], 53 | "ecss/z-index-static": [true, { 54 | "message": (selector) => { 55 | return printMessage("z-index-static", selector); 56 | } 57 | }], 58 | "ecss/overflow-hidden": [true, { 59 | "message": (selector) => { 60 | return printMessage("overflow-hidden", selector); 61 | } 62 | }], 63 | "ecss/large-selector-rule": [true, { 64 | "message": (selector, prop) => { 65 | return printMessage("large-selector-rule", selector, prop); 66 | } 67 | }], 68 | "ecss/content-block": [true, { 69 | "message": (selector, prop) => { 70 | return printMessage("content-block", selector, prop); 71 | } 72 | }], 73 | "ecss/class-numbered": [true, { 74 | "message": (selector) => { 75 | return printMessage("class-numbered", selector); 76 | } 77 | }], 78 | "ecss/not-class": [true, { 79 | "message": (selector) => { 80 | return printMessage("not-class", selector); 81 | } 82 | }], 83 | "ecss/selector-dimensions": [true, { 84 | "message": (selector, prop) => { 85 | return printMessage("selector-dimensions", selector, prop); 86 | } 87 | }], 88 | "ecss/relative-width": [true, { 89 | "message": (selector, prop) => { 90 | return printMessage("relative-width", selector, prop); 91 | } 92 | }], 93 | "ecss/width-100p": [true, { 94 | "message": (selector, prop) => { 95 | return printMessage("width-100p", selector, prop); 96 | } 97 | }], 98 | "ecss/flex-prop": [true, { 99 | "message": (selector, prop) => { 100 | return printMessage("flex-prop", selector, prop); 101 | } 102 | }], 103 | "ecss/flex-children": [true, { 104 | "message": (selector, prop) => { 105 | return printMessage("flex-children", selector, prop); 106 | } 107 | }], 108 | "ecss/flex-shorthand": [true, { 109 | "message": (selector, prop) => { 110 | return printMessage("flex-shorthand", selector, prop); 111 | } 112 | }], 113 | "ecss/technique-centered": [true, { 114 | "message": (selector, prop) => { 115 | return printMessage("technique-centered", selector, prop); 116 | } 117 | }], 118 | "ecss/padding-constraints": [true, { 119 | "message": (selector, prop) => { 120 | return printMessage("padding-constraints", selector, prop); 121 | } 122 | }], 123 | "ecss/component-outside": [true, { 124 | "message": (selector, prop) => { 125 | return printMessage("component-outside", selector, prop); 126 | } 127 | }], 128 | "ecss/spacing-large": [true, { 129 | "message": (selector, prop) => { 130 | return printMessage("spacing-large", selector, prop); 131 | } 132 | }], 133 | "ecss/position-prop": [true, { 134 | "message": (selector, prop) => { 135 | return printMessage("position-prop", selector, prop); 136 | } 137 | }], 138 | "ecss/tag-scoped-class": [true, { 139 | "message": (selector) => { 140 | return printMessage("tag-scoped-class", selector); 141 | } 142 | }], 143 | "ecss/content-float": [true, { 144 | "message": (selector) => { 145 | return printMessage("content-float", selector); 146 | } 147 | }], 148 | "ecss/component-dimensions": [true, { 149 | "message": (selector, prop) => { 150 | return printMessage("component-dimensions", selector, prop); 151 | } 152 | }], 153 | "ecss/content-margin": [true, { 154 | "message": (selector, prop) => { 155 | return printMessage("content-margin", selector, prop); 156 | } 157 | }], 158 | "ecss/class-combined-prefix": [true, { 159 | "message": (selector) => { 160 | return printMessage("class-combined-prefix", selector); 161 | } 162 | }], 163 | "ecss/content-padding": [true, { 164 | "message": (selector) => { 165 | return printMessage("content-padding", selector); 166 | } 167 | }], 168 | "ecss/class-child-prefix": [true, { 169 | "message": (selector) => { 170 | return printMessage("class-child-prefix", selector)} 171 | }], 172 | "ecss/selector-filename": [true, { 173 | "message": (selector, prop) => { 174 | return printMessage("component-selector", selector, prop)}, 175 | "ignoreFiles": ["quarantine.css", "main.css", "base.css", "reset.css", "general.css", "/style/"] 176 | }], 177 | "ecss/commented-code": [true, { 178 | "message": printMessage("commented-code") 179 | }], 180 | "plugin/file-max-lines": [200, { 181 | "ignore": ["comments", "blankLines"], 182 | "severity": "warning", 183 | "message": printMessage("file-lines") 184 | }], 185 | "plugin/z-index-value-constraint": [{ 186 | "min": 0, 187 | "max": 10 188 | }, { 189 | "ignoreValues": [-1], 190 | "message": (selector, prop) => { 191 | return printMessage("z-index-band", selector, prop)} 192 | }], 193 | "function-calc-no-unspaced-operator": [true, { 194 | "message": (selector, prop) => { 195 | return printMessage("calc-unspaced", selector, prop)} 196 | }], 197 | "function-no-unknown": [true, { 198 | "message": (selector) => { 199 | return printMessage("function-unknown", selector)} 200 | }], 201 | 202 | "unit-no-unknown": [true, { 203 | "message": (selector, prop) => { 204 | return printMessage("unit-unknown", selector, prop)} 205 | }], 206 | 207 | "csstree/validator": [{ 208 | "ignoreProperties": ["text-box", "text-wrap", "/container/", "animation-timeline", "animation-range", "view-transition-name"], 209 | "ignoreValue": "clamp", 210 | "ignoreAtrules": ["container", "starting-style", "view-transition"] 211 | }, { 212 | "message": (selector, prop) => { 213 | return printMessage("syntax-invalid", selector, prop)} 214 | }], 215 | "number-max-precision": [5, { 216 | "ignoreUnits": ["em", "rem", "/v/", "s"], 217 | "ignoreProperties": ["/--/"], 218 | "message": (selector, prop) => { 219 | return printMessage("floating-max", selector, prop)} 220 | }], 221 | "declaration-block-no-duplicate-custom-properties": [true, { 222 | "message": (selector) => { 223 | return printMessage("custom-property-duplicate", selector)} 224 | }], 225 | "declaration-block-no-duplicate-properties": [true, { 226 | "message": (selector) => { 227 | return printMessage("property-duplicate", selector)} 228 | }], 229 | "custom-property-no-missing-var-function": [true, { 230 | "message": (selector, prop) => { 231 | return printMessage("var-function-missing", selector, prop)} 232 | }], 233 | "ecss/ignored-properties": [true, { 234 | "message": (selector, prop) => { 235 | return printMessage("property-ignored", selector, prop)} 236 | }], 237 | "ecss/selector-unnecessary": [true, { 238 | "message": (selector, prop) => { 239 | return printMessage("selector-unnecessary", selector, prop)} 240 | }], 241 | "block-no-empty": [true, { 242 | "message": () => { 243 | return printMessage("block-empty")} 244 | }], 245 | 246 | "unit-disallowed-list": [ 247 | ["vh", "vw"], 248 | { 249 | "message": (selector, prop) => { 250 | return printMessage("unit-disallowed", selector, prop)}, 251 | "severity": "warning", 252 | "ignoreFunctions": ["clamp"] 253 | } 254 | ], 255 | "declaration-property-unit-disallowed-list": [{ 256 | "height": ["vh"] 257 | },{ 258 | "message": (selector, prop) => { 259 | return printMessage("property-unit-disallowed", selector, prop)} 260 | }], 261 | "declaration-no-important": [true, { 262 | "message": (selector, prop) => { 263 | return printMessage("declaration-important", selector, prop)} 264 | }], 265 | 266 | "selector-max-compound-selectors": [5, { 267 | "message": (selector, maxValue) => { 268 | return printMessage("selector-max", selector, undefined, maxValue+".")} 269 | }], 270 | "selector-max-class": [3, { 271 | "message": (selector, maxValue) => { 272 | return printMessage("class-max", selector, undefined, maxValue+".")} 273 | }], 274 | "selector-max-type": [4, { 275 | "message": (selector, maxValue) => { 276 | return printMessage("type-max", selector, undefined, maxValue+".")} 277 | }], 278 | "selector-max-universal": [2, { 279 | "message": (selector, prop) => { 280 | return printMessage("universal-max", selector, prop)} 281 | }], 282 | "no-duplicate-at-import-rules": [true, { 283 | "message": (selector, prop) => { 284 | return printMessage("import-duplicate", selector, prop)} 285 | }], 286 | "no-irregular-whitespace": [true, { 287 | "message": (selector, prop) => { 288 | return printMessage("whitespace-irregular", selector, prop)} 289 | }], 290 | "selector-no-qualifying-type": [true, { 291 | "ignore": ["attribute"], 292 | "message": (selector, prop) => { 293 | return printMessage("selector-qualified", selector, prop)} 294 | }], 295 | "selector-max-id": [0, { 296 | "message": (selector, prop) => { 297 | return printMessage("selector-id", selector, prop)} 298 | }], 299 | "selector-nested-pattern": ["&", { 300 | message: printMessage("nesting-pattern"), 301 | "splitList": true 302 | }], 303 | "max-nesting-depth": [3, { 304 | "ignore": ["blockless-at-rules", "pseudo-classes"], 305 | "ignoreRules": ["/:before$/", "/:after$/"], 306 | message: printMessage("nesting-level") 307 | }], 308 | "selector-max-specificity": ["0,2,4", { 309 | "message": (selector) => { 310 | return printMessage("specificity-max", selector)}, 311 | "ignoreSelectors": [/^:(?!.*\b(child|type)\b).+$/] 312 | }], 313 | "magic-numbers/magic-colors": null, 314 | "magic-numbers/magic-numbers": [true, { 315 | "ignoreProperties": ["font-weight", "background", "background-image", "content"], 316 | "acceptedValues": [/[0-9]+0(%|ms|s|ch|px|rem|em|fr|v.*)?/, /(12|13)px/, /^(0|1)?\.[0-9]+(%|ms|s|ch|px|rem|em|v.*)?/], 317 | "acceptedNumbers": [/[0-9]/, /^(0|1)?\.[0-9]*$/], 318 | "message": (selector, prop) => { 319 | return printMessage("magic-number", selector, prop)} 320 | }] 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | testEnvironment: "node", 3 | transform: {}, 4 | testMatch: ["/__tests__/*.test.js"] 5 | }; 6 | -------------------------------------------------------------------------------- /lib/chosenLang.js: -------------------------------------------------------------------------------- 1 | import configLang from "./configLang.js"; 2 | 3 | const { lang } = configLang; 4 | export default function chosenLang() { 5 | let messageLang; 6 | const osLang = Intl.DateTimeFormat().resolvedOptions().locale; 7 | 8 | if(lang == "auto" && (osLang.includes("en-") || osLang.includes("fr-"))){ 9 | messageLang = osLang 10 | } else if(lang == "fr" || lang == "en") { 11 | messageLang = lang; 12 | } else { 13 | messageLang = "en"; 14 | } 15 | return messageLang.split("-")[0]; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /lib/configLang.js: -------------------------------------------------------------------------------- 1 | // Languages for Stylelint messages 2 | // auto (default): checks for OS locale (fr|en) 3 | // fr: french messages 4 | // en: english messages 5 | 6 | export default { 7 | lang: "auto" 8 | }; 9 | -------------------------------------------------------------------------------- /lib/messages.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "generic-error": { 3 | "fr": { 4 | "message": "Erreur générique.", 5 | "url": "https://ecss.info/fr/#erreur-generique" 6 | }, 7 | "en": { 8 | "message": "Generic error.", 9 | "url": "https://ecss.info/en/#generic-error" 10 | } 11 | }, 12 | "content-margin": { 13 | "fr": { 14 | "message": "Seules les marges verticales sont appliquées aux balises de contenu.", 15 | "url": "https://ecss.info/fr/#marges-contenu" 16 | }, 17 | "en": { 18 | "message": "Only vertical margins can be applied to content tags.", 19 | "url": "https://ecss.info/en/#content-margin" 20 | } 21 | }, 22 | "content-block": { 23 | "fr": { 24 | "message": "Les balises de texte devraient demeurer en tant que « block »", 25 | "url": "" 26 | }, 27 | "en": { 28 | "message": "Text tags should remain as \"block\"", 29 | "url": "" 30 | } 31 | }, 32 | "component-selector": { 33 | "fr": { 34 | "message": "Les sélecteurs d'une composante doivent toujours commencer par son nom de fichier.", 35 | "url": "https://ecss.info/fr/#fichier-selecteur" 36 | }, 37 | "en": { 38 | "message": "Selectors for a component must always start with its filename.", 39 | "url": "https://ecss.info/en/#selector-filename" 40 | } 41 | }, 42 | "component-outside": { 43 | "fr": { 44 | "message": "Une composante ne devrait influencer l'extérieur. Son conteneur parent s'occupe du rythme.", 45 | "url": "https://ecss.info/fr/#composants-exterieur" 46 | }, 47 | "en": { 48 | "message": "A component should not influence its external context. Its parent container takes care of the rhythm.", 49 | "url": "https://ecss.info/en/#component-outside" 50 | } 51 | }, 52 | "type-inheritance": { 53 | "fr": { 54 | "message": "Utilisez l'héritage pour vos propriétés typographiques.", 55 | "url": "https://ecss.info/fr/#heritage-texte" 56 | }, 57 | "en": { 58 | "message": "Use inheritance for your typographic properties.", 59 | "url": "https://ecss.info/en/#type-inheritance" 60 | } 61 | }, 62 | "inappropriate-property": { 63 | "fr": { 64 | "message": "Utilisation inappropriée de propriétés.", 65 | "url": "" 66 | }, 67 | "en": { 68 | "message": "Inappropriate use of properties.", 69 | "url": "" 70 | } 71 | }, 72 | "inappropriate-unit": { 73 | "fr": { 74 | "message": "Unités inappropriées.", 75 | "url": "" 76 | }, 77 | "en": { 78 | "message": "Inappropriate units.", 79 | "url": "" 80 | } 81 | }, 82 | "nesting-level": { 83 | "fr": { 84 | "message": "Trois niveaux d'imbrication seulement est accepté.", 85 | "url": "https://ecss.info/fr/#niveau-imbrication" 86 | }, 87 | "en": { 88 | "message": "Only three nesting levels is accepted.", 89 | "url": "https://ecss.info/en/#nesting-level" 90 | } 91 | }, 92 | "nesting-pattern": { 93 | "fr": { 94 | "message": "Commencez les sélecteurs imbriqués par « & »", 95 | "url": "https://ecss.info/fr/#selecteurs-imbriques" 96 | }, 97 | "en": { 98 | "message": "Begin nested selectors with \"&\"", 99 | "url": "https://ecss.info/en/#nested-selectors" 100 | } 101 | }, 102 | "position-sensitive": { 103 | "fr": { 104 | "message": "Cette valeur de positionnement est délicate. Prudence est de mise.", 105 | "url": "" 106 | }, 107 | "en": { 108 | "message": "This positioning value is tricky. Caution is required.", 109 | "url": "" 110 | } 111 | }, 112 | "content-padding": { 113 | "fr": { 114 | "message": "Les dégagements ne vont pas sur le contenu mais sur les conteneurs.", 115 | "url": "https://ecss.info/fr/#degagement-contenu" 116 | }, 117 | "en": { 118 | "message": "The paddings don't go on contents but on containers.", 119 | "url": "https://ecss.info/en/#content-padding" 120 | } 121 | }, 122 | "padding-irregular": { 123 | "fr": { 124 | "message": "Les dégagements devraient être uniformes. Optez pour une marge sinon.", 125 | "url": "https://ecss.info/fr/#degagement-irregulier" 126 | }, 127 | "en": { 128 | "message": "Paddings should be equal. If not, margins may be better suited.", 129 | "url": "https://ecss.info/en/#padding-irregular" 130 | } 131 | }, 132 | "design-token": { 133 | "fr": { 134 | "message": "Utilisez les variables de langage graphique prédéfinies.", 135 | "url": "https://ecss.info/fr/#variables-design" 136 | }, 137 | "en": { 138 | "message": "Use predefined design tokens.", 139 | "url": "https://ecss.info/en/#design-token" 140 | } 141 | }, 142 | "pseudo-disallowed": { 143 | "fr": { 144 | "message": "Cette pseudo-classe devrait généralement être évitée.", 145 | "url": "" 146 | }, 147 | "en": { 148 | "message": "This pseudo-class should be avoided", 149 | "url": "" 150 | } 151 | }, 152 | "property-conjoined": { 153 | "fr": { 154 | "message": "Cette propriété doit être associée à une autre.", 155 | "url": "https://ecss.info/fr/#proprietes-conjointes" 156 | }, 157 | "en": { 158 | "message": "This property should be used with another.", 159 | "url": "https://ecss.info/en/#conjoined-properties" 160 | } 161 | }, 162 | "declaration-important": { 163 | "fr": { 164 | "message": "N'utilisez pas !important.", 165 | "url": "https://ecss.info/fr/#specificite-selecteur" 166 | }, 167 | "en": { 168 | "message": "Do not use !important", 169 | "url": "https://ecss.info/en/#selector-specificity" 170 | } 171 | }, 172 | "specificity-max": { 173 | "fr": { 174 | "message": "Spécificité trop élevée pour le sélecteur.", 175 | "url": "https://ecss.info/fr/#specificite-selecteur" 176 | }, 177 | "en": { 178 | "message": "Specificity too high for the selector.", 179 | "url": "https://ecss.info/en/#selector-specificity" 180 | } 181 | }, 182 | "file-lines": { 183 | "fr": { 184 | "message": "Fichier très volumineux. Devrait être divisé.", 185 | "url": "" 186 | }, 187 | "en": { 188 | "message": "Large file. Should be divided in smaller ones.", 189 | "url": "" 190 | } 191 | }, 192 | "z-index-band": { 193 | "fr": { 194 | "message": "Limitez les niveaux de z-index.", 195 | "url": "" 196 | }, 197 | "en": { 198 | "message": "Limit z-index levels to lower values.", 199 | "url": "" 200 | } 201 | }, 202 | "z-index-static": { 203 | "fr": { 204 | "message": "z-index devrait être accompagné d'une position autre que « static ».", 205 | "url": "https://ecss.info/fr/#proprietes-conjointes" 206 | }, 207 | "en": { 208 | "message": "Expected non-static position for z-index.", 209 | "url": "https://ecss.info/en/#conjoined-properties" 210 | } 211 | }, 212 | "calc-unspaced": { 213 | "fr": { 214 | "message": "Laissez toujours un espace entre vos opérateurs", 215 | "url": "" 216 | }, 217 | "en": { 218 | "message": "Always leave a space between operators.", 219 | "url": "" 220 | } 221 | }, 222 | "function-unknown": { 223 | "fr": { 224 | "message": "N'utilisez pas de fonction inconnue.", 225 | "url": "" 226 | }, 227 | "en": { 228 | "message": "Do not use unknown functions.", 229 | "url": "" 230 | } 231 | }, 232 | "unit-unknown": { 233 | "fr": { 234 | "message": "N'utilisez pas d'unités inconnues.", 235 | "url": "" 236 | }, 237 | "en": { 238 | "message": "Do not use unknown units.", 239 | "url": "" 240 | } 241 | }, 242 | "syntax-invalid": { 243 | "fr": { 244 | "message": "Syntaxe invalide.", 245 | "url": "" 246 | }, 247 | "en": { 248 | "message": "Invalid syntax.", 249 | "url": "" 250 | } 251 | }, 252 | "floating-max": { 253 | "fr": { 254 | "message": "Évitez les décimales en trop grand nombre", 255 | "url": "" 256 | }, 257 | "en": { 258 | "message": "Avoid too many decimals.", 259 | "url": "" 260 | } 261 | }, 262 | "property-duplicate": { 263 | "fr": { 264 | "message": "Ne répétez pas les propriétés dans un même ensemble.", 265 | "url": "" 266 | }, 267 | "en": { 268 | "message": "Do not repeat properties in the same set.", 269 | "url": "" 270 | } 271 | }, 272 | "custom-property-duplicate": { 273 | "fr": { 274 | "message": "Ne répétez pas les propriétés personnalisées dans un même ensemble.", 275 | "url": "" 276 | }, 277 | "en": { 278 | "message": "Do not repeat custom properties in the same set.", 279 | "url": "" 280 | } 281 | }, 282 | "var-function-missing": { 283 | "fr": { 284 | "message": "var() est nécessaire pour utiliser les propriétés personnalisées", 285 | "url": "" 286 | }, 287 | "en": { 288 | "message": "var() function is necessary to use custom properties.", 289 | "url": "" 290 | } 291 | }, 292 | "property-ignored": { 293 | "fr": { 294 | "message": "Combinaison de propriété exclusive", 295 | "url": "" 296 | }, 297 | "en": { 298 | "message": "Ignored property because of exclusive combination.", 299 | "url": "" 300 | } 301 | }, 302 | "block-empty": { 303 | "fr": { 304 | "message": "Retirez les ensembles de règles vides.", 305 | "url": "" 306 | }, 307 | "en": { 308 | "message": "Remove empty declaration blocks.", 309 | "url": "" 310 | } 311 | }, 312 | "unit-disallowed": { 313 | "fr": { 314 | "message": "Vous utilisez des unités fragiles ou erratiques. Attention.", 315 | "url": "" 316 | }, 317 | "en": { 318 | "message": "Be cautious when using fragile units.", 319 | "url": "" 320 | } 321 | }, 322 | "selector-max": { 323 | "fr": { 324 | "message": "Ce sélecteur est trop complexe. Simplifiez ou créez une classe. Maximum de ", 325 | "url": "https://ecss.info/fr/#selecteurs-necessaires" 326 | }, 327 | "en": { 328 | "message": "Selector too complex. Simplify or use a class. Max ", 329 | "url": "https://ecss.info/en/#selector-parts" 330 | } 331 | }, 332 | "class-max": { 333 | "fr": { 334 | "message": "Évitez d'enchaîner autant de classes. Maximum de ", 335 | "url": "https://ecss.info/fr/#specificite-selecteur" 336 | }, 337 | "en": { 338 | "message": "Avoid chaining that many classes. Max ", 339 | "url": "https://ecss.info/en/#selector-specificity" 340 | } 341 | }, 342 | "type-max": { 343 | "fr": { 344 | "message": "Évitez d'enchaîner autant de sélecteurs de balise. Maximum de ", 345 | "url": "https://ecss.info/fr/#selecteurs-necessaires" 346 | }, 347 | "en": { 348 | "message": "Avoid chaining that many tag selectors. Max ", 349 | "url": "https://ecss.info/en/#selector-parts" 350 | } 351 | }, 352 | "universal-max": { 353 | "fr": { 354 | "message": "Évitez d'enchaîner autant de sélecteurs universels", 355 | "url": "" 356 | }, 357 | "en": { 358 | "message": "Avoid chaining that many universal selectors.", 359 | "url": "" 360 | } 361 | }, 362 | "import-duplicate": { 363 | "fr": { 364 | "message": "Ne dupliquez pas les importations", 365 | "url": "" 366 | }, 367 | "en": { 368 | "message": "Do not duplicate imports.", 369 | "url": "" 370 | } 371 | }, 372 | "whitespace-irregular": { 373 | "fr": { 374 | "message": "Gardez vos espaces uniformes.", 375 | "url": "" 376 | }, 377 | "en": { 378 | "message": "Keep spaces uniform.", 379 | "url": "" 380 | } 381 | }, 382 | "content-absolute": { 383 | "fr": { 384 | "message": "Ne pas mettre le contenu en position absolue à moins qu'absolument nécessaire.", 385 | "url": "" 386 | }, 387 | "en": { 388 | "message": "Do not make content absolute unless absolutely necessary.", 389 | "url": "" 390 | } 391 | }, 392 | "selector-unnecessary": { 393 | "fr": { 394 | "message": "N'utilisez que des sélecteurs absolument nécessaires dans vos sélecteurs combinés.", 395 | "url": "https://ecss.info/fr/#selecteurs-necessaires" 396 | }, 397 | "en": { 398 | "message": "Only use selectors that are absolutely necessary in your combined selectors.", 399 | "url": "https://ecss.info/en/#selector-parts" 400 | } 401 | }, 402 | "property-width-height": { 403 | "fr": { 404 | "message": "Évitez tant que possible de donner des dimensions spécifiques sur les deux axes.", 405 | "url": "https://ecss.info/fr/#dimensions-specifiques" 406 | }, 407 | "en": { 408 | "message": "Avoid giving specific dimensions on both axes as much as possible.", 409 | "url": "https://ecss.info/en/#specific-sizes" 410 | } 411 | }, 412 | "relative-width": { 413 | "fr": { 414 | "message": "Préférez « flex-basis » pour des largeurs en pourcentage.", 415 | "url": "" 416 | }, 417 | "en": { 418 | "message": "Prefer “flex-basis” for percentage widths.", 419 | "url": "" 420 | } 421 | }, 422 | "width-100p": { 423 | "fr": { 424 | "message": "« 100% » comme valeur de « width » n'est nécessaire qu'en de rares cas. Essayez plutôt de maintenir « auto ».", 425 | "url": "" 426 | }, 427 | "en": { 428 | "message": "\"100%\" as \"width\" value is only necessary in rare cases. Try to keep \"auto\" instead.", 429 | "url": "" 430 | } 431 | }, 432 | "selector-dimensions": { 433 | "fr": { 434 | "message": "Seuls les éléments graphiques devraient recevoir des hauteurs.", 435 | "url": "https://ecss.info/fr/#dimensions-specifiques" 436 | }, 437 | "en": { 438 | "message": "Only graphical elements should be given heights.", 439 | "url": "https://ecss.info/en/#specific-sizes" 440 | } 441 | }, 442 | "content-float": { 443 | "fr": { 444 | "message": "Le flottement devrait être réservé aux images.", 445 | "url": "" 446 | }, 447 | "en": { 448 | "message": "Floating should be reserved for images.", 449 | "url": "" 450 | } 451 | }, 452 | "overflow-hidden": { 453 | "fr": { 454 | "message": "Évitez de masquer le contenu qui dépasse. Permettez les barres de défilement.", 455 | "url": "https://ecss.info/fr/#debordement-cache" 456 | }, 457 | "en": { 458 | "message": "Avoid hiding excess content. Allow scrollbars.", 459 | "url": "https://ecss.info/en/#overflow-hidden" 460 | } 461 | }, 462 | "technique-centered": { 463 | "fr": { 464 | "message": "Méthode désuète d'alignement centré. Utilisez flex ou grid.", 465 | "url": "https://ecss.info/fr/#technique-centree" 466 | }, 467 | "en": { 468 | "message": "Outdated method of centered alignment. Use flex or grid.", 469 | "url": "https://ecss.info/en/#technique-centered" 470 | } 471 | }, 472 | "flex-shorthand": { 473 | "fr": { 474 | "message": "Préférez les propriétés flex séparées pour une meilleure clarté.", 475 | "url": "https://ecss.info/fr/#flex-court" 476 | }, 477 | "en": { 478 | "message": "Prefer separate flex properties for better clarity.", 479 | "url": "https://ecss.info/en/#flex-shorthand" 480 | } 481 | }, 482 | "selector-id": { 483 | "fr": { 484 | "message": "Évitez les sélecteurs #identifiant et préférez les sélecteurs d'attribut. La spécificité des #identifiant est trop grande.", 485 | "url": "https://ecss.info/fr/#selecteur-id" 486 | }, 487 | "en": { 488 | "message": "Avoid #identifier selectors and prefer attribute selectors. The specificity of #identifiers is too large.", 489 | "url": "https://ecss.info/en/#selector-id" 490 | } 491 | }, 492 | "class-child-prefix": { 493 | "fr": { 494 | "message": "Les classes de descendants devraient toujours être préfixées.", 495 | "url": "https://ecss.info/fr/#entites-prefixees" 496 | }, 497 | "en": { 498 | "message": "Descendant classes should always be prefixed.", 499 | "url": "https://ecss.info/en/#prefixed-entities" 500 | } 501 | }, 502 | "tag-scoped-class": { 503 | "fr": { 504 | "message": "Ne limitez pas vos classes à certaines balises.", 505 | "url": "https://ecss.info/fr/#entites-prefixees" 506 | }, 507 | "en": { 508 | "message": "Do not limit your classes to certain tags.", 509 | "url": "https://ecss.info/en/#prefixed-entities" 510 | } 511 | }, 512 | "class-combined-prefix": { 513 | "fr": { 514 | "message": "Les classes combinées devraient toujours être préfixées.", 515 | "url": "https://ecss.info/fr/#entites-prefixees" 516 | }, 517 | "en": { 518 | "message": "Combined classes should always be prefixed.", 519 | "url": "https://ecss.info/en/#prefixed-entities" 520 | } 521 | }, 522 | "spacing-large": { 523 | "fr": { 524 | "message": "Espacement très large. Les propriétés d'alignement pourraient être appropriées", 525 | "url": "" 526 | }, 527 | "en": { 528 | "message": "Very large spacing. Alignment properties might be better suited", 529 | "url": "" 530 | } 531 | }, 532 | "class-numbered": { 533 | "fr": { 534 | "message": "Évitez les chiffres dans les noms de classe.", 535 | "url": "https://ecss.info/fr/#chiffres-classes" 536 | }, 537 | "en": { 538 | "message": "Avoid numbers in class names.", 539 | "url": "https://ecss.info/en/#numbered-classes" 540 | } 541 | }, 542 | "not-class": { 543 | "fr": { 544 | "message": "Ne pas utiliser d'entités dans le sélecteur :not().", 545 | "url": "" 546 | }, 547 | "en": { 548 | "message": "Do not use entities in the :not() selector.", 549 | "url": "" 550 | } 551 | }, 552 | "component-dimensions": { 553 | "fr": { 554 | "message": "Ne forcez pas de dimensions sur une composante. Limitez-les seulement.", 555 | "url": "https://ecss.info/fr/#dimensions-specifiques" 556 | }, 557 | "en": { 558 | "message": "Do not force size on a component. Only limits.", 559 | "url": "https://ecss.info/en/#specific-size" 560 | } 561 | }, 562 | "large-selector-rule": { 563 | "fr": { 564 | "message": "Évitez de changer des propriétés importantes sur des sélecteurs trop larges.", 565 | "url": "" 566 | }, 567 | "en": { 568 | "message": "Avoid changing important properties on wide selectors.", 569 | "url": "" 570 | } 571 | }, 572 | "magic-number": { 573 | "fr": { 574 | "message": "Évitez d'utiliser des « chiffres magiques » .", 575 | "url": "https://ecss.info/fr/#nombres-magiques" 576 | }, 577 | "en": { 578 | "message": "Avoid using “magic numbers”.", 579 | "url": "https://ecss.info/en/#magic-numbers" 580 | } 581 | }, 582 | "selector-qualified": { 583 | "fr": { 584 | "message": "Attention de ne pas surqualifier vos sélecteurs.", 585 | "url": "https://ecss.info/fr/#selecteurs-surqualifies" 586 | }, 587 | "en": { 588 | "message": "Be careful not to overqualify your selectors.", 589 | "url": "https://ecss.info/en/#overqualified-selectors" 590 | } 591 | }, 592 | "selector-complex": { 593 | "fr": { 594 | "message": "Attention aux sélecteurs trop complexes. ils peuvent généralement être simplifiés.", 595 | "url": "https://ecss.info/fr/#selecteurs-necessaires" 596 | }, 597 | "en": { 598 | "message": "Be careful of overly complex selectors. They can usually be simplified.", 599 | "url": "https://ecss.info/en/#selector-parts" 600 | } 601 | }, 602 | "selector-large": { 603 | "fr": { 604 | "message": "Attention à vos sélecteurs, ils pourraient s'avérer trop larges dans des projets plus complexes.", 605 | "url": "" 606 | }, 607 | "en": { 608 | "message": "Be careful with your selectors, they could turn out to be too wide in more complex projects.", 609 | "url": "" 610 | } 611 | }, 612 | "rule-invalid": { 613 | "fr": { 614 | "message": "Attention à la validité.", 615 | "url": "" 616 | }, 617 | "en": { 618 | "message": "Pay attention to validity.", 619 | "url": "" 620 | } 621 | }, 622 | "selector-specificity": { 623 | "fr": { 624 | "message": "Spécificité trop élevée pour le sélecteur.", 625 | "url": "https://ecss.info/fr/#specificite-selecteur" 626 | }, 627 | "en": { 628 | "message": "Selector specificity is too high.", 629 | "url": "https://ecss.info/en/#selector-specificity" 630 | } 631 | }, 632 | "selector-inadequate": { 633 | "fr": { 634 | "message": "Sélecteur inadéquat.", 635 | "url": "" 636 | }, 637 | "en": { 638 | "message": "Inadequate selector.", 639 | "url": "" 640 | } 641 | }, 642 | "selector-property": { 643 | "fr": { 644 | "message": "Propriété à éviter sur ce sélecteur.", 645 | "url": "" 646 | }, 647 | "en": { 648 | "message": "Property to avoid on this selector.", 649 | "url": "" 650 | } 651 | }, 652 | "property-unit-disallowed": { 653 | "fr": { 654 | "message": "Propriété et unité inappropriées.", 655 | "url": "" 656 | }, 657 | "en": { 658 | "message": "Disallowed property unit combo.", 659 | "url": "" 660 | } 661 | }, 662 | "decimal-value": { 663 | "fr": { 664 | "message": "Valeur décimale non-supportée.", 665 | "url": "" 666 | }, 667 | "en": { 668 | "message": "Unsupported decimal value.", 669 | "url": "" 670 | } 671 | }, 672 | "align-display": { 673 | "fr": { 674 | "message": "Rôle d'affichage « grid » ou « flex » attendu pour les propriétés d'alignement.", 675 | "url": "https://ecss.info/fr/#proprietes-conjointes" 676 | }, 677 | "en": { 678 | "message": "Flexible display or grid expected for alignment or justification properties.", 679 | "url": "https://ecss.info/en/#conjoined-properties" 680 | } 681 | }, 682 | "flex-children": { 683 | "fr": { 684 | "message": "Affichage flex attendu sur le parent pour les propriétés flex.", 685 | "url": "https://ecss.info/fr/#proprietes-conjointes" 686 | }, 687 | "en": { 688 | "message": "Expected flex display on parent for flex properties.", 689 | "url": "https://ecss.info/en/#conjoined-properties" 690 | } 691 | }, 692 | "flex-prop": { 693 | "fr": { 694 | "message": "Affichage flex attendu pour les propriétés flex.", 695 | "url": "https://ecss.info/fr/#proprietes-conjointes" 696 | }, 697 | "en": { 698 | "message": "Expected flex display for flex properties.", 699 | "url": "https://ecss.info/en/#flex-prop" 700 | } 701 | }, 702 | "position-prop": { 703 | "fr": { 704 | "message": "Position non statique attendue pour les propriétés de positionnement.", 705 | "url": "https://ecss.info/fr/#proprietes-conjointes" 706 | }, 707 | "en": { 708 | "message": "Non-static position expected for positioning properties.", 709 | "url": "https://ecss.info/en/#conjoined-properties" 710 | } 711 | }, 712 | "overflow-hidden": { 713 | "fr": { 714 | "message": "« overflow:hidden » devrait n'être utilisé qu'avec « border-radius » ou « aspect-ratio ».", 715 | "url": "https://ecss.info/fr/#debordement-masque" 716 | }, 717 | "en": { 718 | "message": "Expected border-radius or aspect-ratio for overflow hidden.", 719 | "url": "https://ecss.info/en/#overflow-hidden" 720 | } 721 | }, 722 | "padding-constraints": { 723 | "fr": { 724 | "message": "Les dégagements « padding » nécessitent des contraintes graphiques.", 725 | "url": "https://ecss.info/fr/#degagement-irregulier" 726 | }, 727 | "en": { 728 | "message": "Paddings require graphical constraints.", 729 | "url": "https://ecss.info/en/#padding-irregular" 730 | } 731 | }, 732 | "commented-code": { 733 | "fr": { 734 | "message": "Évitez de commenter du code. Effacez-le tout simplement.", 735 | "url": "" 736 | }, 737 | "en": { 738 | "message": "Avoid commenting code. Just delete it.", 739 | "url": "" 740 | } 741 | } 742 | } 743 | -------------------------------------------------------------------------------- /lib/printUrl.js: -------------------------------------------------------------------------------- 1 | import messages from "./messages.js"; 2 | import chosenLang from "./chosenLang.js"; 3 | 4 | export default function printUrl(keywordId) { 5 | const URL = messages[keywordId][chosenLang()].url; 6 | 7 | return URL; 8 | } 9 | -------------------------------------------------------------------------------- /lib/printmessage.js: -------------------------------------------------------------------------------- 1 | import messages from "./messages.js"; 2 | import chosenLang from "./chosenLang.js"; 3 | 4 | export default function printMessage(keywordId, source, problem, customValue) { 5 | let results = messages[keywordId][chosenLang()].message; 6 | if(customValue) { 7 | results += customValue 8 | } 9 | if(source || problem) { 10 | results += " `" 11 | } 12 | if(source) { 13 | results += source 14 | } 15 | if(source && problem) { 16 | results += "` & `" 17 | } 18 | if(problem) { 19 | results += problem 20 | } 21 | if(source || problem) { 22 | results += "`" 23 | } 24 | return results; 25 | } 26 | -------------------------------------------------------------------------------- /lib/selectors.js: -------------------------------------------------------------------------------- 1 | const contentTag_selectorPart = 'p|ul|li|a|button|input|span|h1|h2|h3|h4|h5|h6'; 2 | const structureTag_selectorPart = 'div|header|footer|section|aside|article' 3 | const graphical_selectorPart = 'image|img|video|hr|picture|photo|icon|i$|shape|before$|after$|input|figure|hr$|svg|line|logo|frame|button|input|select|textarea'; 4 | const prefixed_selectorPart = 'is-|as-|on-|to-|with-|and-|now-|fx-|for-|__'; 5 | 6 | export const text_selectors = /^(.*((\s|>|\()(p|h1|h2|h3|h4|h5|h6|blockquote)))\)?$/; 7 | export const structureTag_selectors = new RegExp('^('+structureTag_selectorPart+')$'); 8 | export const numberedClass_selectors = /\.(?!(h[1-6]|grid-[0-9]+|col-[0-9]+)$)[a-zA-Z-]*[0-9]+/; 9 | export const unprefixedDescendant_selectors = new RegExp('^[\\s?>?\\s?|\\s]\\s[.](?!'+prefixed_selectorPart+').*$'); 10 | export const unprefixedCombinedClass_selectors = new RegExp('^(&|[.][a-zA-Z-_]*)[.](?!'+prefixed_selectorPart+').*$'); 11 | export const pseudoClass_selectors = /:.*/; 12 | export const childPseudoClass_selectors = /:.*[child]/; 13 | export const typePseudoClass_selectors = /:.*/; 14 | export const prefixedClass_selectors = /.(${prefixed_selectorPart}).*/; 15 | export const notWithClasses_selectors = /(:not\(.*\.)/; 16 | export const component_selectors = new RegExp('^(?!& )(?!.*__)([.]|\\[[a-z0-9-_]*="?)(?!.*(?:'+graphical_selectorPart+'))[a-zA-Z0-9-_]+("?\\])?$'); 17 | export const notGraphical_selectors = new RegExp('^(?!.*(?:'+graphical_selectorPart+')).*$'); 18 | export const overlyStructuredChildren_selectors = new RegExp('^((.*)[\\s](div|footer|section|aside|article|ul|li).*|body.*)\\b('+contentTag_selectorPart+')\\b$'); 19 | export const tagScopedClass_selectors = new RegExp('^(?![.])(('+structureTag_selectorPart+')( |>| > ))+([.]|\\[[a-z-_]*=?"?).*("?\\])?$') 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@efficientcss/stylelint-config-ecss", 3 | "version": "1.0.0-beta-15", 4 | "description": "Linting rules for EfficientCSS", 5 | "main": "index.js", 6 | "type": "module", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/efficientcss/stylelint-config-ecss.git" 10 | }, 11 | "keywords": [ 12 | "stylelint", 13 | "stylelint-config", 14 | "linting", 15 | "config", 16 | "css", 17 | "efficientcss", 18 | "ecss" 19 | ], 20 | "author": "Marc-André Charpentier", 21 | "files": [ 22 | "!**/__tests__/**", 23 | "lib/**", 24 | "plugins/**", 25 | "index.js" 26 | ], 27 | "scripts": { 28 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" 29 | }, 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/efficientcss/stylelint-config-ecss/issues" 33 | }, 34 | "homepage": "https://ecss.info", 35 | "peerDependencies": { 36 | "stylelint": "^16.0.0" 37 | }, 38 | "dependencies": { 39 | "css-tree": "^2.3.1", 40 | "is-plain-object": "^5.0.0", 41 | "postcss-nested": "^6.2.0", 42 | "stylelint-file-max-lines": "^1.0.0" 43 | }, 44 | "directories": { 45 | "lib": "lib", 46 | "test": "tests" 47 | }, 48 | "devDependencies": { 49 | "jest": "^29.7.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /plugins/ecss-align-display.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import hasPropertyValueInContext from './utils/hasPropertyValueInContext.js'; 3 | import printUrl from '../lib/printUrl.js'; 4 | 5 | const { 6 | createPlugin, 7 | utils: { report, ruleMessages } 8 | } = stylelint; 9 | 10 | const ruleName = 'ecss/align-display'; 11 | const messages = ruleMessages(ruleName, { 12 | expected: (selector) => `Expected "display: flex" or "display: grid" when using alignment or justification properties ${selector}.`, 13 | }); 14 | 15 | const meta = { 16 | url: printUrl('align-display') 17 | } 18 | 19 | 20 | const ruleFunction = () => { 21 | return (postcssRoot, postcssResult) => { 22 | postcssRoot.walkRules((rule) => { 23 | const selectedNodes = rule.nodes.filter((node) => 24 | node.type === 'decl' && ['align-items', 'justify-content', 'gap'].includes(node.prop) 25 | ); 26 | 27 | const hasFlexOrGridDisplay = selectedNodes.length && hasPropertyValueInContext(rule, 'display', /flex|grid/, 'self'); 28 | 29 | selectedNodes.forEach(node => { 30 | if (!hasFlexOrGridDisplay) { 31 | report({ 32 | message: messages.expected, 33 | messageArgs: [rule.selector, node.prop], 34 | node, 35 | result: postcssResult, 36 | ruleName, 37 | }); 38 | } 39 | }); 40 | }); 41 | }; 42 | }; 43 | 44 | ruleFunction.ruleName = ruleName; 45 | ruleFunction.messages = messages; 46 | ruleFunction.meta = meta; 47 | 48 | export default createPlugin(ruleName, ruleFunction); 49 | -------------------------------------------------------------------------------- /plugins/ecss-class-child-prefix.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import printUrl from '../lib/printUrl.js'; 3 | 4 | const { 5 | createPlugin, 6 | utils: { report, ruleMessages } 7 | } = stylelint; 8 | 9 | const ruleName = 'ecss/class-child-prefix'; 10 | const messages = ruleMessages(ruleName, { 11 | rejected: "Descendant classes should always be prefixed.", 12 | }); 13 | 14 | const meta = { 15 | url: printUrl('class-child-prefix') 16 | } 17 | 18 | 19 | const ruleFunction = (primaryOption, secondaryOption, context) => { 20 | return (postcssRoot, postcssResult) => { 21 | const unprefixedDescendantRegex = /(?\s*)\.(?!is-|as-|on-|to-|with-|and-|now-|fx-|for-|__)[a-zA-Z0-9_-]+/; 22 | 23 | postcssRoot.walkRules((rule) => { 24 | for (const selector of rule.selectors) { 25 | if (unprefixedDescendantRegex.test(selector)) { 26 | report({ 27 | message: messages.rejected, 28 | messageArgs: [rule.selector], 29 | node: rule, 30 | result: postcssResult, 31 | ruleName, 32 | }); 33 | } 34 | } 35 | }); 36 | }; 37 | }; 38 | 39 | ruleFunction.ruleName = ruleName; 40 | ruleFunction.messages = messages; 41 | ruleFunction.meta = meta; 42 | 43 | export default createPlugin(ruleName, ruleFunction); 44 | -------------------------------------------------------------------------------- /plugins/ecss-class-combined-prefix.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import printUrl from '../lib/printUrl.js'; 3 | 4 | const { 5 | createPlugin, 6 | utils: { report, ruleMessages } 7 | } = stylelint; 8 | 9 | const ruleName = 'ecss/class-combined-prefix'; 10 | const messages = ruleMessages(ruleName, { 11 | expected: 'Combined classes should always be prefixed.', 12 | }); 13 | 14 | const meta = { 15 | url: printUrl('class-combined-prefix') 16 | } 17 | 18 | 19 | const ruleFunction = (primaryOption, secondaryOption, context) => { 20 | return (postcssRoot, postcssResult) => { 21 | const unprefixedCombinedClassRegex = /^(&|[.][a-zA-Z-_]*)[.](?!is-|as-|on-|to-|with-|and-|now-|fx-|for-|__).*$/; 22 | 23 | postcssRoot.walkRules((rule) => { 24 | if (unprefixedCombinedClassRegex.test(rule.selector)) { 25 | report({ 26 | message: messages.expected, 27 | messageArgs: [rule.selector], 28 | node: rule, 29 | result: postcssResult, 30 | ruleName, 31 | }); 32 | } 33 | }); 34 | }; 35 | }; 36 | 37 | ruleFunction.ruleName = ruleName; 38 | ruleFunction.messages = messages; 39 | ruleFunction.meta = meta; 40 | 41 | export default createPlugin(ruleName, ruleFunction); 42 | -------------------------------------------------------------------------------- /plugins/ecss-class-numbered.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import printUrl from '../lib/printUrl.js'; 3 | 4 | const { 5 | createPlugin, 6 | utils: { report, ruleMessages } 7 | } = stylelint; 8 | 9 | const ruleName = 'ecss/class-numbered'; 10 | const messages = ruleMessages(ruleName, { 11 | expected: 'Avoid numbers in class names.', 12 | }); 13 | 14 | const meta = { 15 | url: printUrl('class-numbered') 16 | } 17 | 18 | 19 | const ruleFunction = (primaryOption, secondaryOption, context) => { 20 | return (postcssRoot, postcssResult) => { 21 | const numberedClassRegex = /^(?!.*(?:h[1-6]|grid-\d+|col-\d+|\(\d+\))).*\d/; 22 | 23 | postcssRoot.walkRules((rule) => { 24 | if (numberedClassRegex.test(rule.selector)) { 25 | report({ 26 | message: messages.expected, 27 | messageArgs: [rule.selector], 28 | node: rule, 29 | result: postcssResult, 30 | ruleName, 31 | }); 32 | } 33 | }); 34 | }; 35 | }; 36 | 37 | ruleFunction.ruleName = ruleName; 38 | ruleFunction.messages = messages; 39 | ruleFunction.meta = meta; 40 | 41 | export default createPlugin(ruleName, ruleFunction); 42 | -------------------------------------------------------------------------------- /plugins/ecss-commented-code.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | 3 | const ruleName = "ecss/commented-code"; 4 | const messages = stylelint.utils.ruleMessages(ruleName, { 5 | rejected: "Unexpected commented-out CSS code" 6 | }); 7 | 8 | const ruleFunction = (primaryOption, secondaryOptions, context) => { 9 | return (root, result) => { 10 | const validOptions = stylelint.utils.validateOptions(result, ruleName, { 11 | actual: primaryOption, 12 | possible: [true, false] 13 | }); 14 | 15 | if (!validOptions) { 16 | return; 17 | } 18 | 19 | 20 | // Regex to identify CSS properties within comments 21 | const commentedCssPattern = /[a-zA-Z-]+\s*:\s*[^;]+;\s*/; 22 | 23 | root.walkComments(comment => { 24 | if (comment.text.match(commentedCssPattern)) { 25 | stylelint.utils.report({ 26 | message: messages.rejected, 27 | messageArgs: [comment], 28 | node: comment, 29 | result, 30 | ruleName 31 | }); 32 | } 33 | }); 34 | }; 35 | }; 36 | 37 | export default stylelint.createPlugin(ruleName, ruleFunction); 38 | export { ruleName, messages }; 39 | -------------------------------------------------------------------------------- /plugins/ecss-component-dimensions.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import printUrl from '../lib/printUrl.js'; 3 | 4 | const { 5 | createPlugin, 6 | utils: { report, ruleMessages } 7 | } = stylelint; 8 | 9 | const ruleName = 'ecss/component-dimensions'; 10 | const messages = ruleMessages(ruleName, { 11 | expected: 'Do not force dimensions on a component. Only limit them.', 12 | }); 13 | 14 | const meta = { 15 | url: printUrl('component-dimensions') 16 | } 17 | 18 | 19 | const ruleFunction = (primaryOption, secondaryOption, context) => { 20 | return (postcssRoot, postcssResult) => { 21 | const componentSelectorsRegex = /^(?!& )(?!.*__)([.]|\\[[a-z0-9-_]*="?)(?!.*(?:image|img|video|hr|picture|photo|icon|i$|shape|before$|after$|input|figure|hr$|svg|line|logo|frame|button|input|select|textarea))[a-zA-Z0-9-_]+("?\\])?$/; 22 | 23 | postcssRoot.walkRules((rule) => { 24 | 25 | const selectedNodes = rule.nodes.filter((node) => 26 | node.type === 'decl' && /^(!?:max-)?(?:width|height)$/.test(node.prop) 27 | ); 28 | 29 | selectedNodes.forEach(node => { 30 | if (componentSelectorsRegex.test(rule.selector)) { 31 | report({ 32 | message: messages.expected, 33 | messageArgs: [rule.selector, node.prop], 34 | node, 35 | result: postcssResult, 36 | ruleName, 37 | }); 38 | } 39 | }); 40 | }); 41 | }; 42 | }; 43 | 44 | ruleFunction.ruleName = ruleName; 45 | ruleFunction.messages = messages; 46 | ruleFunction.meta = meta; 47 | 48 | export default createPlugin(ruleName, ruleFunction); 49 | -------------------------------------------------------------------------------- /plugins/ecss-component-outside.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import printUrl from '../lib/printUrl.js'; 3 | 4 | const { 5 | createPlugin, 6 | utils: { report, ruleMessages } 7 | } = stylelint; 8 | 9 | const ruleName = 'ecss/component-outside'; 10 | const messages = ruleMessages(ruleName, { 11 | expected: 'A component should not influence its external context. Its parent container takes care of the rhythm.', 12 | }); 13 | 14 | const meta = { 15 | url: printUrl('component-outside') 16 | } 17 | 18 | 19 | const ruleFunction = (primaryOption, secondaryOption, context) => { 20 | return (postcssRoot, postcssResult) => { 21 | const componentSelectorsRegex = /^(?!& )(?!.*__)([.]|\\[[a-z0-9-_]*="?)(?!.*(?:image|img|video|hr|picture|photo|icon|i$|shape|before$|after$|input|figure|hr$|svg|line|logo|frame|button|input|select|textarea))[a-zA-Z0-9-_]+("?\\])?$/; 22 | 23 | postcssRoot.walkRules((rule) => { 24 | const selectedNodes = rule.nodes.filter((node) => 25 | node.type === 'decl' && ['margin'].includes(node.prop) 26 | ); 27 | 28 | if (componentSelectorsRegex.test(rule.selector)) { 29 | selectedNodes.forEach(decl => { 30 | report({ 31 | message: messages.expected, 32 | messageArgs: [rule.selector, decl], 33 | node: decl, 34 | result: postcssResult, 35 | ruleName, 36 | }); 37 | }); 38 | } 39 | }); 40 | }; 41 | }; 42 | 43 | ruleFunction.ruleName = ruleName; 44 | ruleFunction.messages = messages; 45 | ruleFunction.meta = meta; 46 | 47 | export default createPlugin(ruleName, ruleFunction); 48 | -------------------------------------------------------------------------------- /plugins/ecss-content-block.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | 3 | const { 4 | createPlugin, 5 | utils: { report, ruleMessages } 6 | } = stylelint; 7 | 8 | const ruleName = 'ecss/content-block'; 9 | const messages = ruleMessages(ruleName, { 10 | expected: 'Text tags should remain as "block" unless including a pseudo-element.', 11 | }); 12 | 13 | const meta = { 14 | url: '' 15 | }; 16 | 17 | const textTagRegex = /^(.*((\s|>|\()(p|h1|h2|h3|h4|h5|h6|blockquote)))\)?$/; 18 | const pseudoElementRegex = /^&:{1,2}(before|after)$/; 19 | 20 | const ruleFunction = (primaryOption, secondaryOption, context) => { 21 | return (postcssRoot, postcssResult) => { 22 | postcssRoot.walkRules((rule) => { 23 | rule.walkDecls('display', (decl) => { 24 | const displayValue = decl.value; 25 | const selector = rule.selector; 26 | 27 | if (!textTagRegex.test(selector)) return; 28 | if (/^(block|inline|inline-block)$/.test(displayValue)) return; 29 | 30 | const hasPseudoElementChild = rule.nodes.some( 31 | node => 32 | node.type === 'rule' && 33 | pseudoElementRegex.test(node.selector) 34 | ); 35 | 36 | if (!hasPseudoElementChild) { 37 | report({ 38 | message: messages.expected, 39 | messageArgs: [rule.selector, decl], 40 | node: decl, 41 | result: postcssResult, 42 | ruleName, 43 | }); 44 | } 45 | }); 46 | }); 47 | }; 48 | }; 49 | 50 | ruleFunction.ruleName = ruleName; 51 | ruleFunction.messages = messages; 52 | ruleFunction.meta = meta; 53 | 54 | export default createPlugin(ruleName, ruleFunction); 55 | -------------------------------------------------------------------------------- /plugins/ecss-content-float.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import postcss from "postcss"; 3 | import nested from "postcss-nested"; 4 | 5 | const { 6 | createPlugin, 7 | utils: { report, ruleMessages } 8 | } = stylelint; 9 | 10 | const ruleName = 'ecss/content-float'; 11 | const messages = ruleMessages(ruleName, { 12 | expected: 'Floating should be reserved for images.', 13 | }); 14 | 15 | const meta = { 16 | url: '' 17 | }; 18 | 19 | const preprocessCSS = async (css) => { 20 | const result = await postcss([nested]).process(css, { from: undefined }); 21 | return result.root; 22 | }; 23 | 24 | const ruleFunction = (primaryOption, secondaryOption, context) => async (postcssRoot, postcssResult) => { 25 | const notGraphicalSelectorsRegex = /^(?!.*(?:image|img|video|hr|picture|photo|icon|i$|shape|before$|after$|input|figure|hr$|svg|line|logo|frame|button|input|select|textarea)).*$/; 26 | const processedRoot = await preprocessCSS(postcssRoot.toString()); 27 | 28 | processedRoot.walkRules((rule) => { 29 | rule.walkDecls('float', (decl) => { 30 | if (notGraphicalSelectorsRegex.test(rule.selector)) { 31 | report({ 32 | message: messages.expected, 33 | messageArgs: [rule.selector, decl], 34 | node: decl, 35 | result: postcssResult, 36 | ruleName, 37 | }); 38 | } 39 | }); 40 | }); 41 | }; 42 | 43 | ruleFunction.ruleName = ruleName; 44 | ruleFunction.messages = messages; 45 | ruleFunction.meta = meta; 46 | 47 | export default createPlugin(ruleName, ruleFunction); 48 | -------------------------------------------------------------------------------- /plugins/ecss-content-margin.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import printUrl from '../lib/printUrl.js'; 3 | 4 | const { 5 | createPlugin, 6 | utils: { report, ruleMessages } 7 | } = stylelint; 8 | 9 | const ruleName = 'ecss/content-margin'; 10 | const messages = ruleMessages(ruleName, { 11 | expected: 'Only vertical margins (top/bottom) can be applied to content tags.', 12 | }); 13 | 14 | const meta = { 15 | url: printUrl('content-margin') 16 | } 17 | 18 | 19 | const ruleFunction = (primaryOption, secondaryOption, context) => { 20 | return (postcssRoot, postcssResult) => { 21 | const textTagRegex = /^(.*((\s|>|\()(p|h1|h2|h3|h4|h5|h6|blockquote)))\)?$/; 22 | 23 | postcssRoot.walkRules((rule) => { 24 | const selectedNodes = rule.nodes.filter((node) => 25 | node.type === 'decl' && /^margin/.test(node.prop) 26 | ); 27 | 28 | selectedNodes.forEach(node => { 29 | if (textTagRegex.test(rule.selector) && !/^(margin-top|margin-bottom|margin-block(?:-start|-end)?)$/.test(node.prop)) { 30 | report({ 31 | message: messages.expected, 32 | messageArgs: [rule.selector, node], 33 | node: node, 34 | result: postcssResult, 35 | ruleName, 36 | }); 37 | } 38 | }); 39 | }); 40 | }; 41 | }; 42 | 43 | ruleFunction.ruleName = ruleName; 44 | ruleFunction.messages = messages; 45 | ruleFunction.meta = meta; 46 | 47 | export default createPlugin(ruleName, ruleFunction); 48 | -------------------------------------------------------------------------------- /plugins/ecss-content-padding.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import printUrl from '../lib/printUrl.js'; 3 | 4 | const { 5 | createPlugin, 6 | utils: { report, ruleMessages } 7 | } = stylelint; 8 | 9 | const ruleName = 'ecss/content-padding'; 10 | const messages = ruleMessages(ruleName, { 11 | expected: "The paddings don't go on contents but on containers.", 12 | }); 13 | 14 | const meta = { 15 | url: printUrl('content-padding') 16 | } 17 | 18 | 19 | const ruleFunction = (primaryOption, secondaryOption, context) => { 20 | return (postcssRoot, postcssResult) => { 21 | const textTagRegex = /^(.*((\s|>|\()(p|h1|h2|h3|h4|h5|h6|blockquote)))\)?$/; 22 | 23 | postcssRoot.walkRules((rule) => { 24 | const selectedNodes = rule.nodes.filter((node) => 25 | node.type === 'decl' && /^padding/.test(node.prop) 26 | ); 27 | selectedNodes.forEach(node => { 28 | if (textTagRegex.test(rule.selector)) { 29 | report({ 30 | message: messages.expected, 31 | messageArgs: [rule.selector, node], 32 | node: node, 33 | result: postcssResult, 34 | ruleName, 35 | }); 36 | } 37 | }); 38 | }); 39 | }; 40 | }; 41 | 42 | ruleFunction.ruleName = ruleName; 43 | ruleFunction.messages = messages; 44 | ruleFunction.meta = meta; 45 | 46 | export default createPlugin(ruleName, ruleFunction); 47 | -------------------------------------------------------------------------------- /plugins/ecss-flex-children.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import hasPropertyValueInContext from './utils/hasPropertyValueInContext.js'; 3 | import printUrl from '../lib/printUrl.js'; 4 | 5 | const { 6 | createPlugin, 7 | utils: { report, ruleMessages } 8 | } = stylelint; 9 | 10 | const ruleName = 'ecss/flex-children'; 11 | const messages = ruleMessages(ruleName, { 12 | expected: (selector, prop) => `${selector} ${prop}, Expected "display: flex" on parent when using flex properties like flex-grow, flex-shrink, or flex-wrap in a child selector block.` 13 | }); 14 | 15 | const meta = { 16 | url: printUrl('flex-children') 17 | } 18 | 19 | 20 | const ruleFunction = (primaryOption, secondaryOption, context) => { 21 | return (postcssRoot, postcssResult) => { 22 | postcssRoot.walkRules((rule) => { 23 | 24 | const selectedNodes = rule.nodes.filter((node) => 25 | node.type === 'decl' && ['flex-grow', 'flex-basis', 'flex-shrink', 'align-self'].includes(node.prop) 26 | ); 27 | 28 | const parentHasFlexDisplay = selectedNodes.length && hasPropertyValueInContext(rule, 'display', /flex/, 'parent'); 29 | 30 | selectedNodes.forEach(node => { 31 | if (!parentHasFlexDisplay) { 32 | 33 | report({ 34 | message: messages.expected, 35 | messageArgs: [rule.selector, node.prop], 36 | node, 37 | result: postcssResult, 38 | ruleName, 39 | }); 40 | } 41 | 42 | }); 43 | }); 44 | }; 45 | }; 46 | 47 | ruleFunction.ruleName = ruleName; 48 | ruleFunction.messages = messages; 49 | ruleFunction.meta = meta; 50 | 51 | export default createPlugin(ruleName, ruleFunction); 52 | -------------------------------------------------------------------------------- /plugins/ecss-flex-prop.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import hasPropertyValueInContext from './utils/hasPropertyValueInContext.js'; 3 | import printUrl from '../lib/printUrl.js'; 4 | 5 | const { 6 | createPlugin, 7 | utils: { report, ruleMessages } 8 | } = stylelint; 9 | 10 | const ruleName = 'ecss/flex-prop'; 11 | const messages = ruleMessages(ruleName, { 12 | expected: 'Expected "display: flex" when using flex properties like flex-direction, flex-wrap, or flex-flow in a self-combined context without child selectors.', 13 | }); 14 | 15 | const meta = { 16 | url: printUrl('flex-prop') 17 | } 18 | 19 | 20 | const ruleFunction = (primaryOption, secondaryOption, context) => { 21 | return (postcssRoot, postcssResult) => { 22 | postcssRoot.walkRules((rule) => { 23 | const selectedNodes = rule.nodes.filter((node) => 24 | node.type === 'decl' && ['flex-direction', 'flex-flow', 'flex-wrap'].includes(node.prop) 25 | ); 26 | 27 | const hasFlexDisplay = selectedNodes.length && hasPropertyValueInContext(rule, 'display', 'flex', 'self'); 28 | 29 | selectedNodes.forEach(node => { 30 | if (!hasFlexDisplay) { 31 | report({ 32 | message: messages.expected, 33 | messageArgs: [rule.selector, node.prop], 34 | node, 35 | result: postcssResult, 36 | ruleName, 37 | }); 38 | } 39 | }); 40 | }); 41 | }; 42 | }; 43 | 44 | ruleFunction.ruleName = ruleName; 45 | ruleFunction.messages = messages; 46 | ruleFunction.meta = meta; 47 | 48 | export default createPlugin(ruleName, ruleFunction); 49 | -------------------------------------------------------------------------------- /plugins/ecss-flex-shorthand.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import postcss from "postcss"; 3 | import nested from "postcss-nested"; 4 | 5 | const { 6 | createPlugin, 7 | utils: { report, ruleMessages } 8 | } = stylelint; 9 | 10 | const ruleName = 'ecss/flex-shorthand'; 11 | const messages = ruleMessages(ruleName, { 12 | expected: 'Prefer long form flex properties for better clarity.', 13 | }); 14 | 15 | const meta = { 16 | url: '' 17 | } 18 | 19 | const preprocessCSS = async (css) => { 20 | const result = await postcss([nested]).process(css, { from: undefined }); 21 | return result.root; 22 | };; 23 | 24 | const ruleFunction = (primaryOption, secondaryOption, context) => async (postcssRoot, postcssResult) => { 25 | const processedRoot = await preprocessCSS(postcssRoot.toString()); 26 | processedRoot.walkRules((rule) => { 27 | rule.walkDecls('flex', (decl) => { 28 | report({ 29 | message: messages.expected, 30 | messageArgs: [rule.selector, decl], 31 | node: decl, 32 | result: postcssResult, 33 | ruleName, 34 | }); 35 | }); 36 | }); 37 | }; 38 | 39 | ruleFunction.ruleName = ruleName; 40 | ruleFunction.messages = messages; 41 | ruleFunction.meta = meta; 42 | 43 | export default createPlugin(ruleName, ruleFunction); 44 | -------------------------------------------------------------------------------- /plugins/ecss-ignored-properties.js: -------------------------------------------------------------------------------- 1 | import stylelint from "stylelint"; 2 | import matchesStringOrRegExp from "./utils/matchesStringOrRegExp.js"; 3 | import vendorPrefixes from "./utils/vendorPrefixes.js"; 4 | 5 | const { 6 | createPlugin, 7 | utils: { report, ruleMessages, validateOptions } 8 | } = stylelint; 9 | 10 | const ruleName = "ecss/ignored-properties"; 11 | 12 | const messages = ruleMessages(ruleName, { 13 | rejected: (ignore, cause) => `Unexpected "${ignore}" with "${cause}"`, 14 | }); 15 | 16 | const ignored = [ 17 | { 18 | property: "display", 19 | value: "inline", 20 | ignoredProperties: [ 21 | "width", 22 | "min-width", 23 | "max-width", 24 | "height", 25 | "min-height", 26 | "max-height", 27 | "margin", 28 | "margin-top", 29 | "margin-bottom", 30 | "overflow", 31 | "overflow-x", 32 | "overflow-y", 33 | "inline-size", 34 | "min-inline-size", 35 | "max-inline-size", 36 | "block-size", 37 | "min-block-size", 38 | "max-block-size", 39 | "margin-block-start", 40 | "margin-block-end", 41 | "overflow-block", 42 | "overflow-inline", 43 | ], 44 | }, 45 | { 46 | property: "display", 47 | value: "list-item", 48 | ignoredProperties: ["vertical-align"], 49 | }, 50 | { 51 | property: "display", 52 | value: "block", 53 | ignoredProperties: ["vertical-align"], 54 | }, 55 | { 56 | property: "display", 57 | value: "flex", 58 | ignoredProperties: ["vertical-align"], 59 | }, 60 | { 61 | property: "display", 62 | value: "table", 63 | ignoredProperties: ["vertical-align"], 64 | }, 65 | { 66 | property: "display", 67 | value: 68 | "/^table-(row|row-group|column|column-group|header-group|footer-group|cell)$/", 69 | ignoredProperties: [ 70 | "margin", 71 | "margin-top", 72 | "margin-right", 73 | "margin-bottom", 74 | "margin-left", 75 | "margin-block-start", 76 | "margin-inline-end", 77 | "margin-block-end", 78 | "margin-inline-start", 79 | ], 80 | }, 81 | { 82 | property: "display", 83 | value: 84 | "/^table-(row|row-group|column|column-group|header-group|footer-group)$/", 85 | ignoredProperties: [ 86 | "padding", 87 | "padding-top", 88 | "padding-right", 89 | "padding-bottom", 90 | "padding-left", 91 | "padding-block-start", 92 | "padding-inline-end", 93 | "padding-block-end", 94 | "padding-inline-start", 95 | ], 96 | }, 97 | { 98 | property: "display", 99 | value: 100 | "/^table-(row|row-group|column|column-group|header-group|footer-group|caption)$/", 101 | ignoredProperties: ["vertical-align"], 102 | }, 103 | { 104 | property: "display", 105 | value: "/^table-(row|row-group)$/", 106 | ignoredProperties: [ 107 | "width", 108 | "min-width", 109 | "max-width", 110 | "inline-size", 111 | "min-inline-size", 112 | "max-inline-size", 113 | ], 114 | }, 115 | { 116 | property: "display", 117 | value: "/^table-(column|column-group)$/", 118 | ignoredProperties: [ 119 | "height", 120 | "min-height", 121 | "max-height", 122 | "block-size", 123 | "min-block-size", 124 | "max-block-size", 125 | ], 126 | }, 127 | { 128 | property: "float", 129 | value: "left", 130 | ignoredProperties: ["vertical-align"], 131 | }, 132 | { 133 | property: "float", 134 | value: "right", 135 | ignoredProperties: ["vertical-align"], 136 | }, 137 | { 138 | property: "position", 139 | value: "static", 140 | ignoredProperties: [ 141 | "top", 142 | "right", 143 | "bottom", 144 | "left", 145 | "z-index", 146 | "inset-block-start", 147 | "inset-inline-end", 148 | "inset-block-end", 149 | "inset-inline-start", 150 | ], 151 | }, 152 | { 153 | property: "position", 154 | value: "absolute", 155 | ignoredProperties: ["float", "clear", "vertical-align"], 156 | }, 157 | { 158 | property: "position", 159 | value: "fixed", 160 | ignoredProperties: ["float", "clear", "vertical-align"], 161 | }, 162 | { 163 | property: "list-style-type", 164 | value: "none", 165 | ignoredProperties: ["list-style-image"], 166 | }, 167 | { 168 | property: "overflow", 169 | value: "visible", 170 | ignoredProperties: ["resize"], 171 | }, 172 | ]; 173 | 174 | const rule = (actual) => { 175 | return (root, result) => { 176 | const validOptions = validateOptions(result, ruleName, { actual }); 177 | 178 | if (!validOptions) { 179 | return; 180 | } 181 | 182 | root.walkRules((rule) => { 183 | const uniqueDecls = {}; 184 | rule.walkDecls((decl) => { 185 | uniqueDecls[decl.prop] = decl; 186 | }); 187 | 188 | function check(prop, index) { 189 | const decl = uniqueDecls[prop]; 190 | const value = decl.value; 191 | const unprefixedProp = vendorPrefixes.unprefixed(prop); 192 | const unprefixedValue = vendorPrefixes.unprefixed(value); 193 | 194 | ignored.forEach((ignore) => { 195 | const matchProperty = matchesStringOrRegExp( 196 | unprefixedProp.toLowerCase(), 197 | ignore.property 198 | ); 199 | const matchValue = matchesStringOrRegExp( 200 | unprefixedValue.toLowerCase(), 201 | ignore.value 202 | ); 203 | 204 | if (!matchProperty || !matchValue) { 205 | return; 206 | } 207 | 208 | const ignoredProperties = ignore.ignoredProperties; 209 | 210 | decl.parent.nodes.forEach((node, nodeIndex) => { 211 | if ( 212 | !node.prop || 213 | ignoredProperties.indexOf(node.prop.toLowerCase()) === -1 || 214 | index === nodeIndex 215 | ) { 216 | return; 217 | } 218 | 219 | report({ 220 | messageArgs: [node.prop, decl.toString()], 221 | message: messages.rejected(node.prop, decl.toString()), 222 | node, 223 | result, 224 | ruleName, 225 | }); 226 | }); 227 | }); 228 | } 229 | 230 | Object.keys(uniqueDecls).forEach(check); 231 | }); 232 | }; 233 | }; 234 | 235 | rule.ruleName = ruleName; 236 | rule.messages = messages; 237 | export default createPlugin(ruleName, rule); 238 | -------------------------------------------------------------------------------- /plugins/ecss-large-selector-rule.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | 3 | const { 4 | createPlugin, 5 | utils: { report, ruleMessages } 6 | } = stylelint; 7 | 8 | const ruleName = 'ecss/large-selector-rule'; 9 | const messages = ruleMessages(ruleName, { 10 | expected: 'Avoid changing important properties on wide selectors.', 11 | }); 12 | 13 | const meta = { 14 | url: '' 15 | }; 16 | 17 | const ruleFunction = (primaryOption, secondaryOption, context) => { 18 | return (postcssRoot, postcssResult) => { 19 | const structureTagRegex = /^(div|header|footer|section|aside|article)$/; 20 | const propertyRegex = /^(position|background|display|padding|margin|width|height|border|shadow)/; 21 | 22 | postcssRoot.walkRules((rule) => { 23 | 24 | const selectedNodes = rule.nodes.filter((node) => 25 | node.type === 'decl' && node.prop.match(propertyRegex)); 26 | selectedNodes.forEach(node => { 27 | if (structureTagRegex.test(rule.selector)) { 28 | report({ 29 | message: messages.expected, 30 | messageArgs: [rule.selector, node.prop], 31 | node, 32 | result: postcssResult, 33 | ruleName, 34 | }); 35 | } 36 | }); 37 | }); 38 | }; 39 | }; 40 | 41 | ruleFunction.ruleName = ruleName; 42 | ruleFunction.messages = messages; 43 | ruleFunction.meta = meta; 44 | 45 | export default createPlugin(ruleName, ruleFunction); 46 | -------------------------------------------------------------------------------- /plugins/ecss-not-class.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import postcss from "postcss"; 3 | import nested from "postcss-nested"; 4 | 5 | const { 6 | createPlugin, 7 | utils: { report, ruleMessages } 8 | } = stylelint; 9 | 10 | const ruleName = 'ecss/not-class'; 11 | const messages = ruleMessages(ruleName, { 12 | expected: 'Do not use entities in the :not() selector.', 13 | }); 14 | 15 | const meta = { 16 | url: '' 17 | }; 18 | 19 | const preprocessCSS = async (css) => { 20 | const result = await postcss([nested]).process(css, { from: undefined }); 21 | return result.root; 22 | };; 23 | 24 | const ruleFunction = (primaryOption, secondaryOption, context) => async (postcssRoot, postcssResult) => { 25 | const processedRoot = await preprocessCSS(postcssRoot.toString()); 26 | const notWithClassesRegex = /:not\([^)]*(?:\.\w|\[\w+=)/; 27 | 28 | processedRoot.walkRules((rule) => { 29 | if (notWithClassesRegex.test(rule.selector)) { 30 | report({ 31 | message: messages.expected, 32 | messageArgs: [rule.selector], 33 | node: rule, 34 | result: postcssResult, 35 | ruleName, 36 | }); 37 | } 38 | }); 39 | }; 40 | 41 | ruleFunction.ruleName = ruleName; 42 | ruleFunction.messages = messages; 43 | ruleFunction.meta = meta; 44 | 45 | export default createPlugin(ruleName, ruleFunction); 46 | -------------------------------------------------------------------------------- /plugins/ecss-overflow-hidden.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import hasPropertyValueInContext from './utils/hasPropertyValueInContext.js'; 3 | import printUrl from '../lib/printUrl.js'; 4 | 5 | const { 6 | createPlugin, 7 | utils: { report, ruleMessages } 8 | } = stylelint; 9 | 10 | const ruleName = 'ecss/overflow-hidden'; 11 | const messages = ruleMessages(ruleName, { 12 | expected: 'Expected "border-radius" or "aspect-ratio" when using "overflow: hidden".', 13 | }); 14 | 15 | const meta = { 16 | url: printUrl('overflow-hidden') 17 | } 18 | 19 | 20 | const ruleFunction = (primaryOption, secondaryOption, context) => { 21 | return (postcssRoot, postcssResult) => { 22 | postcssRoot.walkRules((rule) => { 23 | const selectedNodes = rule.nodes.filter((node) => 24 | node.type === 'decl' && ['overflow'].includes(node.prop) && ['hidden'].includes(node.value) 25 | ); 26 | const ignoreSelectors = /picture/; 27 | const hasNeeded = hasPropertyValueInContext(rule, /radius|aspect/, /.*/, 'self'); 28 | 29 | selectedNodes.forEach(node => { 30 | if (!hasNeeded && !ignoreSelectors.test(rule.selector)) { 31 | report({ 32 | message: messages.expected, 33 | messageArgs: [rule.selector], 34 | node, 35 | result: postcssResult, 36 | ruleName, 37 | }); 38 | } 39 | }); 40 | }); 41 | }; 42 | }; 43 | 44 | ruleFunction.ruleName = ruleName; 45 | ruleFunction.messages = messages; 46 | ruleFunction.meta = meta; 47 | 48 | export default createPlugin(ruleName, ruleFunction); 49 | -------------------------------------------------------------------------------- /plugins/ecss-padding-constraints.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import hasPropertyValueInContext from './utils/hasPropertyValueInContext.js'; 3 | import printUrl from '../lib/printUrl.js'; 4 | 5 | const { 6 | createPlugin, 7 | utils: { report, ruleMessages } 8 | } = stylelint; 9 | 10 | const ruleName = 'ecss/padding-constraints'; 11 | const messages = ruleMessages(ruleName, { 12 | expected: 'Paddings should require graphical constraints.', 13 | }); 14 | 15 | const meta = { 16 | url: printUrl('padding-constraints') 17 | } 18 | 19 | 20 | const ruleFunction = (primaryOption, secondaryOption, context) => { 21 | return (postcssRoot, postcssResult) => { 22 | const ignoreSelectorsRegex = /(>|\s)?(is:|where:)?.*(a|ul|ol|button|input)(\))?(:.*)?$/; 23 | 24 | postcssRoot.walkRules((rule) => { 25 | if (!ignoreSelectorsRegex.test(rule.selector)) { 26 | const selectedNodes = rule.nodes.filter((node) => 27 | node.type === 'decl' && /^padding/.test(node.prop) 28 | ); 29 | 30 | const hasNeededProp = selectedNodes.length && hasPropertyValueInContext(rule, /(text-indent|background|border|margin|box-sizing|overflow)/, /.*/, 'self'); 31 | 32 | selectedNodes.forEach(node => { 33 | if (!hasNeededProp) { 34 | report({ 35 | message: messages.expected, 36 | messageArgs: [rule.selector, node.prop], 37 | node, 38 | result: postcssResult, 39 | ruleName, 40 | }); 41 | } 42 | }); 43 | } 44 | }); 45 | }; 46 | }; 47 | 48 | ruleFunction.ruleName = ruleName; 49 | ruleFunction.messages = messages; 50 | ruleFunction.meta = meta; 51 | 52 | export default createPlugin(ruleName, ruleFunction); 53 | -------------------------------------------------------------------------------- /plugins/ecss-position-prop.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import hasPropertyValueInContext from './utils/hasPropertyValueInContext.js'; 3 | import printUrl from '../lib/printUrl.js'; 4 | 5 | const { 6 | createPlugin, 7 | utils: { report, ruleMessages } 8 | } = stylelint; 9 | 10 | const ruleName = 'ecss/position-prop'; 11 | const messages = ruleMessages(ruleName, { 12 | expected: 'Non-static position expected for positioning properties in a self-combined context without child selectors.', 13 | }); 14 | 15 | const meta = { 16 | url: printUrl('position-prop') 17 | } 18 | 19 | 20 | const ruleFunction = (primaryOption, secondaryOption, context) => { 21 | return (postcssRoot, postcssResult) => { 22 | postcssRoot.walkRules((rule) => { 23 | const selectedNodes = rule.nodes.filter((node) => 24 | node.type === 'decl' && ['top', 'left', 'right', 'bottom', 'inset'].includes(node.prop) 25 | ); 26 | 27 | const hasNonStaticPosition = selectedNodes.length && hasPropertyValueInContext(rule, 'position', /^(?!.*\bstatic\b).+$/, 'self'); 28 | 29 | selectedNodes.forEach(node => { 30 | if (!hasNonStaticPosition) { 31 | report({ 32 | message: messages.expected, 33 | messageArgs: [rule.selector, node], 34 | node, 35 | result: postcssResult, 36 | ruleName, 37 | }); 38 | } 39 | }); 40 | }); 41 | }; 42 | }; 43 | 44 | ruleFunction.ruleName = ruleName; 45 | ruleFunction.messages = messages; 46 | ruleFunction.meta = meta; 47 | 48 | export default createPlugin(ruleName, ruleFunction); 49 | -------------------------------------------------------------------------------- /plugins/ecss-position-sensitive.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import postcss from "postcss"; 3 | import nested from "postcss-nested"; 4 | 5 | const { 6 | createPlugin, 7 | utils: { report, ruleMessages } 8 | } = stylelint; 9 | 10 | const ruleName = 'ecss/position-sensitive'; 11 | const messages = ruleMessages(ruleName, { 12 | expected: 'This positioning value is tricky. Caution is required.', 13 | }); 14 | 15 | const meta = { 16 | url: '' 17 | }; 18 | 19 | const preprocessCSS = async (css) => { 20 | const result = await postcss([nested]).process(css, { from: undefined }); 21 | return result.root; 22 | };; 23 | 24 | const ruleFunction = (primaryOption, secondaryOption, context) => async (postcssRoot, postcssResult) => { 25 | const processedRoot = await preprocessCSS(postcssRoot.toString()); 26 | processedRoot.walkRules((rule) => { 27 | rule.walkDecls('position', (decl) => { 28 | if (/absolute|fixed/.test(decl.value)) { 29 | report({ 30 | message: messages.expected, 31 | messageArgs: [rule.selector, decl], 32 | node: decl, 33 | result: postcssResult, 34 | ruleName, 35 | }); 36 | } 37 | }); 38 | }); 39 | }; 40 | 41 | ruleFunction.ruleName = ruleName; 42 | ruleFunction.messages = messages; 43 | ruleFunction.meta = meta; 44 | 45 | export default createPlugin(ruleName, ruleFunction); 46 | -------------------------------------------------------------------------------- /plugins/ecss-pseudo-disallowed.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | 3 | const { 4 | createPlugin, 5 | utils: { report, ruleMessages } 6 | } = stylelint; 7 | 8 | const ruleName = 'ecss/pseudo-disallowed'; 9 | const messages = ruleMessages(ruleName, { 10 | expected: 'This pseudo-class should be avoided.', 11 | }); 12 | 13 | const ruleFunction = (primaryOption, secondaryOption, context) => { 14 | return (postcssRoot, postcssResult) => { 15 | const disallowedPseudoRegex = /:(first-child|last-child)/; 16 | 17 | postcssRoot.walkRules((rule) => { 18 | if (disallowedPseudoRegex.test(rule.selector)) { 19 | report({ 20 | message: messages.expected, 21 | messageArgs: [rule.selector], 22 | node: rule, 23 | result: postcssResult, 24 | ruleName, 25 | }); 26 | } 27 | }); 28 | }; 29 | }; 30 | 31 | ruleFunction.ruleName = ruleName; 32 | ruleFunction.messages = messages; 33 | 34 | export default createPlugin(ruleName, ruleFunction); 35 | -------------------------------------------------------------------------------- /plugins/ecss-relative-width.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import postcss from "postcss"; 3 | import nested from "postcss-nested"; 4 | 5 | const { 6 | createPlugin, 7 | utils: { report, ruleMessages } 8 | } = stylelint; 9 | 10 | const ruleName = 'ecss/relative-width'; 11 | const messages = ruleMessages(ruleName, { 12 | expected: 'Prefer "flex-basis" for percentage widths.', 13 | }); 14 | 15 | const meta = { 16 | url: '' 17 | }; 18 | 19 | const preprocessCSS = async (css) => { 20 | const result = await postcss([nested]).process(css, { from: undefined }); 21 | return result.root; 22 | };; 23 | 24 | 25 | const ruleFunction = (primaryOption, secondaryOption, context) => async (postcssRoot, postcssResult) => { 26 | const processedRoot = await preprocessCSS(postcssRoot.toString()); 27 | processedRoot.walkRules((rule) => { 28 | rule.walkDecls('width', (decl) => { 29 | if (/^(?!100%)\d+%$/.test(decl.value)) { 30 | report({ 31 | message: messages.expected, 32 | messageArgs: [rule.selector, decl], 33 | node: decl, 34 | result: postcssResult, 35 | ruleName, 36 | }); 37 | } 38 | }); 39 | }); 40 | }; 41 | 42 | ruleFunction.ruleName = ruleName; 43 | ruleFunction.messages = messages; 44 | ruleFunction.meta = meta; 45 | 46 | export default createPlugin(ruleName, ruleFunction); 47 | -------------------------------------------------------------------------------- /plugins/ecss-selector-dimensions.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import postcss from "postcss"; 3 | import nested from "postcss-nested"; 4 | import hasPropertyValueInContext from './utils/hasPropertyValueInContext.js'; 5 | import printUrl from '../lib/printUrl.js'; 6 | 7 | 8 | const { 9 | createPlugin, 10 | utils: { report, ruleMessages } 11 | } = stylelint; 12 | 13 | const ruleName = 'ecss/selector-dimensions'; 14 | const messages = ruleMessages(ruleName, { 15 | expected: 'Only graphical elements should be given heights.', 16 | }); 17 | 18 | const meta = { 19 | url: printUrl('selector-dimensions') 20 | } 21 | 22 | const preprocessCSS = async (css) => { 23 | const result = await postcss([nested]).process(css, { from: undefined }); 24 | return result.root; 25 | }; 26 | 27 | const ruleFunction = (primaryOption, secondaryOption, context) => async (postcssRoot, postcssResult) => { 28 | const notGraphicalSelectorsRegex = /^(?!.*(?:image|img|video|hr|picture|photo|icon|i$|shape|before$|after$|figure|hr$|svg|line|logo|frame|button|input|select$|textarea)).*$/; 29 | const componentSelectorsRegex = /^(?!& )(?!.*__)([.]|\\[[a-z0-9-_]*="?)(?!.*(?:image|img|video|hr|picture|photo|icon|i$|shape|before$|after$|input|figure|hr$|svg|line|logo|frame|button|input|select|textarea))[a-zA-Z0-9-_]+("?\\])?$/; 30 | const processedRoot = await preprocessCSS(postcssRoot.toString()); 31 | 32 | processedRoot.walkRules((rule) => { 33 | 34 | const selectedNodes = rule.nodes.filter((node) => 35 | node.type === 'decl' && /^(?:max-)?(?:height)$/.test(node.prop) 36 | ); 37 | 38 | const hasNeededProp = selectedNodes.length && hasPropertyValueInContext(rule, /(text-indent|background|border|margin|box-sizing|overflow)/, /.*/, 'self'); 39 | 40 | selectedNodes.forEach(node => { 41 | if (notGraphicalSelectorsRegex.test(rule.selector) && !componentSelectorsRegex.test(rule.selector) && !hasNeededProp) { 42 | report({ 43 | message: messages.expected, 44 | messageArgs: [rule.selector, node], 45 | node, 46 | result: postcssResult, 47 | ruleName, 48 | }); 49 | } 50 | }); 51 | }); 52 | }; 53 | 54 | ruleFunction.ruleName = ruleName; 55 | ruleFunction.messages = messages; 56 | ruleFunction.meta = meta; 57 | 58 | export default createPlugin(ruleName, ruleFunction); 59 | -------------------------------------------------------------------------------- /plugins/ecss-selector-filename.js: -------------------------------------------------------------------------------- 1 | import stylelint from "stylelint"; 2 | import postcss from "postcss"; 3 | import nested from "postcss-nested"; 4 | import path from "path"; 5 | import isKeyframeSelector from './utils/isKeyframeSelector.js'; 6 | import optionsMatches from './utils/optionsMatches.js'; 7 | import printUrl from '../lib/printUrl.js'; 8 | 9 | const { 10 | createPlugin, 11 | utils: { report, ruleMessages, validateOptions } 12 | } = stylelint; 13 | 14 | const ruleName = 'ecss/selector-filename'; 15 | const messages = ruleMessages(ruleName, { 16 | rejected: (selector, filename) => `All selectors must begin with filename. ${selector} vs. ${filename}` 17 | }); 18 | 19 | const meta = { 20 | url: printUrl('component-selector') 21 | } 22 | 23 | const isString = (value) => typeof value === 'string' || value instanceof String; 24 | const isRegExp = value => toString.call(value) === '[object RegExp]'; 25 | 26 | const preprocessCSS = async (css) => { 27 | const result = await postcss([nested]).process(css, { from: undefined }); 28 | return result.root; 29 | }; 30 | 31 | const rule = (primary, secondaryOptions) => async (root, result) => { 32 | const validOptions = validateOptions( 33 | result, 34 | ruleName, 35 | { actual: primary }, 36 | { 37 | actual: secondaryOptions, 38 | possible: { ignoreFiles: [isString, isRegExp] }, 39 | optional: true, 40 | }, 41 | ); 42 | 43 | if (!validOptions) return; 44 | 45 | const inputFile = path.basename(root.source.input.file); 46 | 47 | const escapeRegex = (str) => { 48 | return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 49 | }; 50 | 51 | const filename = path.parse(inputFile).name.replace(/^_+/, '').split('.')[0]; 52 | const escapedFilename = escapeRegex(filename); 53 | const selectorPattern = `^&?\\s*:?\\s*(is\\(|where\\()?((\\* \\+ |\\* ~ )?(\\.[_]?|\\[.*=)?)?(\\")?${escapedFilename}(?!(,\\s*\\.[^${escapedFilename}].*)+)(?:-[a-zA-Z]+)?(\\")?(\\])?.*`; 54 | const selectorRegExp = new RegExp(selectorPattern); 55 | 56 | if (optionsMatches(secondaryOptions, 'ignoreFiles', inputFile) || /^\d|^[A-Za-z]-/.test(filename)) return; 57 | 58 | const processedRoot = await preprocessCSS(root.toString()); 59 | 60 | processedRoot.walkRules((rule) => { 61 | rule.selectors.forEach((selector) => { 62 | if (!selectorRegExp.test(selector) && !isKeyframeSelector(selector)) { 63 | report({ 64 | messageArgs: [selector, filename], 65 | message: messages.rejected(selector, filename), 66 | node: rule, 67 | word: selector, 68 | result, 69 | ruleName, 70 | }); 71 | } 72 | }); 73 | }); 74 | }; 75 | 76 | rule.ruleName = ruleName; 77 | rule.messages = messages; 78 | rule.meta = meta; 79 | export default createPlugin(ruleName, rule); 80 | -------------------------------------------------------------------------------- /plugins/ecss-selector-unnecessary.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import printUrl from '../lib/printUrl.js'; 3 | 4 | const { 5 | createPlugin, 6 | utils: { report, ruleMessages } 7 | } = stylelint; 8 | 9 | const ruleName = 'ecss/selector-unnecessary'; 10 | const messages = ruleMessages(ruleName, { 11 | expected: 'Only use selectors that are absolutely necessary in your combined selectors.', 12 | }); 13 | 14 | const meta = { 15 | url: printUrl('selector-unnecessary') 16 | } 17 | 18 | 19 | const ruleFunction = (primaryOption, secondaryOption, context) => { 20 | return (postcssRoot, postcssResult) => { 21 | const overlyStructuredChildrenRegex = /^(?!.*(?:>|~))(?=.*\S\s+\b(? { 24 | if (overlyStructuredChildrenRegex.test(rule.selector) && !necessaryStructuredChildrenRegex.test(rule.selector)) { 25 | report({ 26 | message: messages.expected, 27 | messageArgs: [rule.selector], 28 | node: rule, 29 | result: postcssResult, 30 | ruleName, 31 | }); 32 | } 33 | }); 34 | }; 35 | }; 36 | 37 | ruleFunction.ruleName = ruleName; 38 | ruleFunction.messages = messages; 39 | ruleFunction.meta = meta; 40 | 41 | export default createPlugin(ruleName, ruleFunction); 42 | -------------------------------------------------------------------------------- /plugins/ecss-spacing-large.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | 3 | const { 4 | createPlugin, 5 | utils: { report, ruleMessages } 6 | } = stylelint; 7 | 8 | const ruleName = 'ecss/spacing-large'; 9 | const messages = ruleMessages(ruleName, { 10 | expected: 'Very large spacing. May indicate a composition problem.', 11 | }); 12 | 13 | const meta = { 14 | url: '' 15 | }; 16 | 17 | const ruleFunction = (primaryOption, secondaryOption, context) => { 18 | return (postcssRoot, postcssResult) => { 19 | postcssRoot.walkRules((rule) => { 20 | rule.walkDecls(/^(margin|padding)(-(left|right|inline(-end|start)?))?$/, (decl) => { 21 | if (/^-?(\d{2,}(em|rem)|\d{3,}px)/.test(decl.value)) { 22 | report({ 23 | message: messages.expected, 24 | messageArgs: [rule.selector, decl], 25 | node: decl, 26 | result: postcssResult, 27 | ruleName, 28 | }); 29 | } 30 | }); 31 | }); 32 | }; 33 | }; 34 | 35 | ruleFunction.ruleName = ruleName; 36 | ruleFunction.messages = messages; 37 | ruleFunction.meta = meta; 38 | 39 | export default createPlugin(ruleName, ruleFunction); 40 | -------------------------------------------------------------------------------- /plugins/ecss-stylelint.config.js: -------------------------------------------------------------------------------- 1 | // stylelint.config.js 2 | export default { 3 | "plugins": [ 4 | "./content-block.js" 5 | "./content-margin.js" 6 | "./content-float.js" 7 | "./content-padding.js" 8 | "./selector-dimensions.js" 9 | "./component-outside.js" 10 | "./component-dimensions.js" 11 | "./spacing-large.js" 12 | "./position-sensitive.js" 13 | "./technique-centered.js" 14 | "./flex-shorthand.js" 15 | "./relative-width.js" 16 | "./pseudo-disallowed.js" 17 | "./class-numbered.js" 18 | "./not-class.js" 19 | "./class-child-prefix.js" 20 | "./class-combined-prefix.js" 21 | "./tag-scoped-class.js" 22 | "./selector-unnecessary.js" 23 | "./z-index-static.js" 24 | "./padding-constraints.js" 25 | "./align-display.js" 26 | "./flex-prop.js" 27 | "./position-prop.js" 28 | "./overflow-hidden.js" 29 | ], 30 | "rules": { 31 | "ecss/content-block": true, 32 | "ecss/content-margin": true, 33 | "ecss/content-float": true, 34 | "ecss/content-padding": true, 35 | "ecss/selector-dimensions": true, 36 | "ecss/component-outside": true, 37 | "ecss/component-dimensions": true, 38 | "ecss/spacing-large": true, 39 | "ecss/position-sensitive": true, 40 | "ecss/technique-centered": true, 41 | "ecss/flex-shorthand": true, 42 | "ecss/relative-width": true, 43 | "ecss/pseudo-disallowed": true, 44 | "ecss/class-numbered": true, 45 | "ecss/not-class": true, 46 | "ecss/class-child-prefix": true, 47 | "ecss/class-combined-prefix": true, 48 | "ecss/tag-scoped-class": true, 49 | "ecss/selector-unnecessary": true, 50 | "ecss/z-index-static": true, 51 | "ecss/padding-constraints": true, 52 | "ecss/align-display": true, 53 | "ecss/flex-prop": true, 54 | "ecss/position-prop": true, 55 | "ecss/overflow-hidden": true 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /plugins/ecss-tag-scoped-class.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import printUrl from '../lib/printUrl.js'; 3 | 4 | const { 5 | createPlugin, 6 | utils: { report, ruleMessages } 7 | } = stylelint; 8 | 9 | const ruleName = 'ecss/tag-scoped-class'; 10 | const messages = ruleMessages(ruleName, { 11 | expected: "Don't scope class/attribute selectors to some tags only.", 12 | }); 13 | 14 | const meta = { 15 | url: printUrl('tag-scoped-class') 16 | } 17 | 18 | 19 | const ruleFunction = (primaryOption, secondaryOption, context) => { 20 | return (postcssRoot, postcssResult) => { 21 | const tagScopedClassRegex = /^(?![.])(nav|div|header|footer|section|aside|article)( |>| > )+([.]|\\[[a-z-_]*=?"?).*("?\\])?$/; 22 | 23 | postcssRoot.walkRules((rule) => { 24 | // Check if the selector contains a class or attribute scoped by a tag 25 | if (tagScopedClassRegex.test(rule.selector)) { 26 | report({ 27 | message: messages.expected, 28 | messageArgs: [rule.selector], 29 | node: rule, 30 | result: postcssResult, 31 | ruleName, 32 | }); 33 | } 34 | }); 35 | }; 36 | }; 37 | 38 | ruleFunction.ruleName = ruleName; 39 | ruleFunction.messages = messages; 40 | ruleFunction.meta = meta; 41 | 42 | export default createPlugin(ruleName, ruleFunction); 43 | -------------------------------------------------------------------------------- /plugins/ecss-technique-centered.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | 3 | const { 4 | createPlugin, 5 | utils: { report, ruleMessages } 6 | } = stylelint; 7 | 8 | const ruleName = 'ecss/technique-centered'; 9 | const messages = ruleMessages(ruleName, { 10 | expected: 'Outdated method of centered alignment. Use flex or grid.', 11 | }); 12 | 13 | const meta = { 14 | url: '' 15 | }; 16 | 17 | const ruleFunction = (primaryOption, secondaryOption, context) => { 18 | return (postcssRoot, postcssResult) => { 19 | postcssRoot.walkRules((rule) => { 20 | rule.walkDecls('transform', (decl) => { 21 | if (/translate\(-50%/.test(decl.value)) { 22 | report({ 23 | message: messages.expected, 24 | messageArgs: [rule.selector, decl], 25 | node: decl, 26 | result: postcssResult, 27 | ruleName, 28 | }); 29 | } 30 | }); 31 | }); 32 | }; 33 | }; 34 | 35 | ruleFunction.ruleName = ruleName; 36 | ruleFunction.messages = messages; 37 | ruleFunction.meta = meta; 38 | 39 | export default createPlugin(ruleName, ruleFunction); 40 | -------------------------------------------------------------------------------- /plugins/ecss-width-100p.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import postcss from "postcss"; 3 | import nested from "postcss-nested"; 4 | 5 | const { 6 | createPlugin, 7 | utils: { report, ruleMessages } 8 | } = stylelint; 9 | 10 | const ruleName = 'ecss/width-100p'; 11 | const messages = ruleMessages(ruleName, { 12 | expected: '`width: 100%` is generally unnecessary. The default value `auto` shoudl be kept.', 13 | }); 14 | 15 | const meta = { 16 | url: '' 17 | }; 18 | 19 | const preprocessCSS = async (css) => { 20 | const result = await postcss([nested]).process(css, { from: undefined }); 21 | return result.root; 22 | };; 23 | 24 | 25 | const ruleFunction = (primaryOption, secondaryOption, context) => async (postcssRoot, postcssResult) => { 26 | const processedRoot = await preprocessCSS(postcssRoot.toString()); 27 | const notGraphicalSelectorsRegex = /^(?!.*(?:image|img|video|hr|picture|photo|icon|i$|shape|before$|after$|figure|hr$|svg|line|logo|frame|button|input|select$|textarea)).*$/; 28 | processedRoot.walkRules((rule) => { 29 | rule.walkDecls('width', (decl) => { 30 | if (/^100%$/.test(decl.value) && notGraphicalSelectorsRegex.test(rule.selector)) { 31 | report({ 32 | message: messages.expected, 33 | messageArgs: [rule.selector, decl], 34 | node: decl, 35 | result: postcssResult, 36 | ruleName, 37 | }); 38 | } 39 | }); 40 | }); 41 | }; 42 | 43 | ruleFunction.ruleName = ruleName; 44 | ruleFunction.messages = messages; 45 | ruleFunction.meta = meta; 46 | 47 | export default createPlugin(ruleName, ruleFunction); 48 | -------------------------------------------------------------------------------- /plugins/ecss-z-index-static.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import hasPropertyValueInContext from './utils/hasPropertyValueInContext.js'; 3 | import printUrl from '../lib/printUrl.js'; 4 | 5 | const { 6 | createPlugin, 7 | utils: { report, ruleMessages } 8 | } = stylelint; 9 | 10 | const ruleName = 'ecss/z-index-static'; 11 | const messages = ruleMessages(ruleName, { 12 | expected: 'Non-static position expected when using "z-index".', 13 | }); 14 | 15 | const meta = { 16 | url: printUrl('z-index-static') 17 | } 18 | 19 | 20 | const ruleFunction = () => { 21 | return (postcssRoot, postcssResult) => { 22 | postcssRoot.walkRules((rule) => { 23 | const selectedNodes = rule.nodes.filter((node) => 24 | node.type === 'decl' && ['z-index'].includes(node.prop) 25 | ); 26 | 27 | const hasNonStaticPosition = selectedNodes.length && hasPropertyValueInContext(rule, 'position', /^(?!.*\bstatic\b).+$/, 'self'); 28 | 29 | selectedNodes.forEach(node => { 30 | if (!hasNonStaticPosition) { 31 | report({ 32 | message: messages.expected, 33 | messageArgs: [rule.selector, node], 34 | node, 35 | result: postcssResult, 36 | ruleName, 37 | }); 38 | } 39 | }); 40 | }); 41 | }; 42 | }; 43 | 44 | ruleFunction.ruleName = ruleName; 45 | ruleFunction.messages = messages; 46 | ruleFunction.meta = meta; 47 | 48 | export default createPlugin(ruleName, ruleFunction); 49 | -------------------------------------------------------------------------------- /plugins/stylelint-csstree-validator.js: -------------------------------------------------------------------------------- 1 | import stylelint from 'stylelint'; 2 | import isStandardSyntaxAtRule from 'stylelint/lib/utils/isStandardSyntaxAtRule.mjs'; 3 | import isStandardSyntaxDeclaration from 'stylelint/lib/utils/isStandardSyntaxDeclaration.mjs'; 4 | import isStandardSyntaxProperty from 'stylelint/lib/utils/isStandardSyntaxProperty.mjs'; 5 | import isStandardSyntaxValue from 'stylelint/lib/utils/isStandardSyntaxValue.mjs'; 6 | import { fork, lexer, parse } from 'css-tree'; 7 | import { less, sass } from './utils/syntax-extension/index.js'; 8 | 9 | const { utils, createPlugin } = stylelint; 10 | const isRegExp = value => toString.call(value) === '[object RegExp]'; 11 | const getRaw = (node, name) => (node.raws && node.raws[name]) || ''; 12 | const allowedSyntaxExtensions = new Set(['less', 'sass']); 13 | const lessExtendedSyntax = fork(less); 14 | const sassExtendedSyntax = fork(sass); 15 | const syntaxExtensions = { 16 | none: { fork, lexer, parse }, 17 | less: lessExtendedSyntax, 18 | sass: sassExtendedSyntax, 19 | all: fork(less).fork(sass) 20 | }; 21 | 22 | const ruleName = 'csstree/validator'; 23 | const messages = utils.ruleMessages(ruleName, { 24 | csstree(value) { 25 | return value; 26 | }, 27 | parseError(value) { 28 | return 'Can\'t parse value "' + value + '"'; 29 | }, 30 | unknownAtrule(atrule) { 31 | return 'Unknown at-rule `@' + atrule + '`'; 32 | }, 33 | invalidPrelude(atrule) { 34 | return 'Invalid prelude for `@' + atrule + '`'; 35 | }, 36 | unknownProperty(property) { 37 | return 'Unknown property `' + property + '`'; 38 | }, 39 | invalidValue(property) { 40 | return 'Invalid value for "' + property + '"'; 41 | } 42 | }); 43 | 44 | function createIgnoreMatcher(patterns) { 45 | if (Array.isArray(patterns)) { 46 | const names = new Set(); 47 | const regexpes = []; 48 | 49 | for (let pattern of patterns) { 50 | if (typeof pattern === 'string') { 51 | const stringifiedRegExp = pattern.match(/^\/(.+)\/([a-z]*)/); 52 | 53 | if (stringifiedRegExp) { 54 | regexpes.push(new RegExp(stringifiedRegExp[1], stringifiedRegExp[2])); 55 | } else if (/[^a-z0-9\-]/i.test(pattern)) { 56 | regexpes.push(new RegExp(`^(${pattern})$`, 'i')); 57 | } else { 58 | names.add(pattern.toLowerCase()); 59 | } 60 | } else if (isRegExp(pattern)) { 61 | regexpes.push(pattern); 62 | } 63 | } 64 | 65 | const matchRegExpes = regexpes.length 66 | ? name => regexpes.some(pattern => pattern.test(name)) 67 | : null; 68 | 69 | if (names.size > 0) { 70 | return matchRegExpes !== null 71 | ? name => names.has(name.toLowerCase()) || matchRegExpes(name) 72 | : name => names.has(name.toLowerCase()); 73 | } else if (matchRegExpes !== null) { 74 | return matchRegExpes; 75 | } 76 | } 77 | 78 | return false; 79 | } 80 | 81 | const plugin = createPlugin(ruleName, function(options) { 82 | options = options || {}; 83 | 84 | const optionSyntaxExtension = new Set(Array.isArray(options.syntaxExtensions) ? options.syntaxExtensions : []); 85 | 86 | const ignoreValue = options.ignoreValue && (typeof options.ignoreValue === 'string' || isRegExp(options.ignoreValue)) 87 | ? new RegExp(options.ignoreValue) 88 | : false; 89 | const ignoreProperties = createIgnoreMatcher(options.ignoreProperties || options.ignore); 90 | const ignoreAtrules = createIgnoreMatcher(options.ignoreAtrules); 91 | const atrulesValidationDisabled = options.atrules === false; 92 | const syntax = optionSyntaxExtension.has('less') 93 | ? optionSyntaxExtension.has('sass') 94 | ? syntaxExtensions.all 95 | : syntaxExtensions.less 96 | : optionSyntaxExtension.has('sass') 97 | ? syntaxExtensions.sass 98 | : syntaxExtensions.none; 99 | const lexer = !options.properties && !options.types && !options.atrules 100 | ? syntax.lexer // default syntax 101 | : syntax.fork({ 102 | properties: options.properties || {}, 103 | types: options.types || {}, 104 | atrules: options.atrules || {} 105 | }).lexer; 106 | 107 | return function(root, result) { 108 | const ignoreAtruleNodes = new WeakSet(); 109 | 110 | stylelint.utils.validateOptions(result, ruleName, { 111 | actual: { 112 | ignore: options.ignore, 113 | syntaxExtensions: [...optionSyntaxExtension] 114 | }, 115 | possible: { 116 | ignore: value => value === undefined, 117 | syntaxExtensions: value => allowedSyntaxExtensions.has(value) 118 | } 119 | }); 120 | 121 | root.walkAtRules(function(atrule) { 122 | let error; 123 | 124 | // ignore non-standard at-rules 125 | if (syntax !== syntaxExtensions.none && !isStandardSyntaxAtRule(atrule)) { 126 | return; 127 | } 128 | 129 | // at-rule validation is disabled 130 | if (atrulesValidationDisabled) { 131 | ignoreAtruleNodes.add(atrule); 132 | return; 133 | } 134 | 135 | if (ignoreAtrules !== false && ignoreAtrules(atrule.name)) { 136 | ignoreAtruleNodes.add(atrule); 137 | return; 138 | } 139 | 140 | if (error = lexer.checkAtruleName(atrule.name)) { 141 | ignoreAtruleNodes.add(atrule); 142 | utils.report({ 143 | messageArgs: [atrule], 144 | ruleName, 145 | result, 146 | message: messages.csstree(error.message), 147 | node: atrule 148 | }); 149 | 150 | return; 151 | } 152 | 153 | if (error = lexer.matchAtrulePrelude(atrule.name, atrule.params).error) { 154 | let message = error.rawMessage || error.message; 155 | let index = 2 + atrule.name.length + getRaw('afterName').length; 156 | 157 | if (message === 'Mismatch') { 158 | // ignore mismatch errors for a prelude with syntax extensions 159 | if (syntax !== syntaxExtensions.none && atrule.params) { 160 | try { 161 | syntax.parse(atrule.params, { 162 | atrule: 'unknown', // to use default parsing rules 163 | context: 'atrulePrelude' 164 | }); 165 | } catch (e) { 166 | if (e.type === 'PreprocessorExtensionError') { 167 | return; 168 | } 169 | } 170 | } 171 | 172 | message = messages.invalidPrelude(atrule.name); 173 | index += error.mismatchOffset; 174 | } else { 175 | message = messages.csstree(message); 176 | } 177 | 178 | utils.report({ 179 | messageArgs: [atrule], 180 | ruleName, 181 | result, 182 | message, 183 | node: atrule, 184 | index 185 | }); 186 | } 187 | }); 188 | 189 | root.walkDecls((decl) => { 190 | // don't check for descriptors in bad at-rules 191 | if (ignoreAtruleNodes.has(decl.parent)) { 192 | return; 193 | } 194 | 195 | // ignore properties from ignore list 196 | if (ignoreProperties !== false && ignoreProperties(decl.prop)) { 197 | return; 198 | } 199 | 200 | // ignore declarations with non-standard syntax (Less, Sass, etc) 201 | if (syntax !== syntaxExtensions.none) { 202 | if (!isStandardSyntaxDeclaration(decl) || 203 | !isStandardSyntaxProperty(decl.prop) || 204 | !isStandardSyntaxValue(decl.value)) { 205 | return; 206 | } 207 | } 208 | 209 | try { 210 | syntax.parse(decl.value, { 211 | context: 'value' 212 | }); 213 | } catch (e) { 214 | // ignore values with preprocessor's extensions 215 | if (e.type === 'PreprocessorExtensionError') { 216 | return; 217 | } 218 | 219 | // ignore values by a pattern 220 | if (ignoreValue && ignoreValue.test(decl.value)) { 221 | return; 222 | } 223 | 224 | return utils.report({ 225 | messageArgs: [decl.value], 226 | message: messages.parseError(decl.value), 227 | node: decl, 228 | result, 229 | ruleName 230 | }); 231 | } 232 | 233 | const { error } = decl.parent.type === 'atrule' 234 | ? lexer.matchAtruleDescriptor(decl.parent.name, decl.prop, decl.value) 235 | : lexer.matchProperty(decl.prop, decl.value); 236 | 237 | if (error) { 238 | let message = error.rawMessage || error.message || error; 239 | let index = undefined; 240 | 241 | // ignore errors except those which make sense 242 | if (error.name !== 'SyntaxMatchError' && 243 | error.name !== 'SyntaxReferenceError') { 244 | return; 245 | } 246 | 247 | if (message === 'Mismatch') { 248 | // ignore values by a pattern 249 | if (ignoreValue && ignoreValue.test(decl.value)) { 250 | return; 251 | } 252 | 253 | message = messages.invalidValue(decl.prop); 254 | index = decl.prop.length + getRaw(decl, 'between').length + error.mismatchOffset; 255 | } else { 256 | message = messages.csstree(message); 257 | } 258 | 259 | utils.report({ 260 | messageArgs: [decl.prop, decl.value], 261 | ruleName, 262 | result, 263 | message, 264 | node: decl, 265 | index 266 | }); 267 | } 268 | }); 269 | }; 270 | }); 271 | 272 | export default Object.assign(plugin, { 273 | ruleName, 274 | messages 275 | }); 276 | -------------------------------------------------------------------------------- /plugins/stylelint-declaration-block-conjoined-properties.js: -------------------------------------------------------------------------------- 1 | import stylelint from "stylelint"; 2 | import matchesStringOrRegExp from "./utils/matchesStringOrRegExp.js"; 3 | 4 | const { 5 | createPlugin, 6 | utils: { report, ruleMessages, validateOptions }, 7 | } = stylelint; 8 | 9 | const ruleName = "plugin/declaration-block-conjoined-properties"; 10 | 11 | const messages = ruleMessages(ruleName, { 12 | rejected: (needed, cause) => `Expected any of "${needed}" with "${cause}"`, 13 | }); 14 | 15 | const rule = (primary) => { 16 | return (root, result) => { 17 | const validOptions = validateOptions(result, ruleName, { 18 | actual: primary, 19 | possible: validateNeededObject 20 | }); 21 | 22 | if (!validOptions) { 23 | return; 24 | } 25 | 26 | const needed = primary; 27 | 28 | const processRule = (rule, parentRule = null) => { 29 | const uniqueDecls = new Map(); 30 | 31 | // Collect all declarations in the current rule 32 | rule.walkDecls((decl) => { 33 | uniqueDecls.set(decl.prop, decl); 34 | }); 35 | 36 | // Check each declaration 37 | uniqueDecls.forEach((decl, prop) => { 38 | const value = decl.value.toLowerCase(); 39 | 40 | Object.entries(needed).forEach(([property, neededEntry]) => { 41 | const matchProperty = matchesStringOrRegExp(prop.toLowerCase(), property); 42 | const matchValue = matchesStringOrRegExp(value, neededEntry.value); 43 | 44 | if (!matchProperty || !matchValue) return; 45 | 46 | let hasNeeded = false; 47 | 48 | // Determine selector type and scope 49 | const isChildSelector = /&[\s>]/.test(rule.selector); 50 | const isSelfCombinedSelector = rule.selector.includes("&") && !isChildSelector; 51 | const scope = neededEntry.scope || "self"; 52 | let checkNode = scope === "parent" && isChildSelector && parentRule ? parentRule : rule; 53 | 54 | // Walk through the selected checkNode and look for needed declarations 55 | checkNode.walkDecls((node) => { 56 | neededEntry.neededDeclaration.forEach((obj) => { 57 | const propertyMatches = new RegExp(obj.property, "g").test(node.prop.toLowerCase()); 58 | const valueMatches = new RegExp(obj.value, "g").test(node.value.toLowerCase()); 59 | 60 | if (propertyMatches && valueMatches) { 61 | hasNeeded = true; 62 | } 63 | }); 64 | }); 65 | 66 | // For self-combined selectors, consider them valid if scope is "self" 67 | if (isSelfCombinedSelector && scope === "self") { 68 | hasNeeded = true; 69 | } 70 | 71 | if (!hasNeeded) { 72 | report({ 73 | message: typeof neededEntry.message === 'function' 74 | ? neededEntry.message(prop, value) 75 | : neededEntry.message || messages.rejected(neededEntry.neededDeclaration.map(decl => decl.property), decl.toString()), 76 | node: decl, 77 | result, 78 | ruleName, 79 | }); 80 | } 81 | }); 82 | }); 83 | 84 | // Process child rules recursively, passing the current rule as the parent 85 | rule.walkRules((childRule) => { 86 | processRule(childRule, rule); 87 | }); 88 | }; 89 | 90 | // Start processing from the root 91 | root.walkRules(processRule); 92 | }; 93 | }; 94 | 95 | rule.ruleName = ruleName; 96 | rule.messages = messages; 97 | export default createPlugin(ruleName, rule); 98 | 99 | function validateNeededObject(value) { 100 | return typeof value === 'object' && !Array.isArray(value) && Object.values(value).every(item => { 101 | return item.value && Array.isArray(item.neededDeclaration); 102 | }); 103 | } 104 | -------------------------------------------------------------------------------- /plugins/stylelint-magic-numbers.js: -------------------------------------------------------------------------------- 1 | import stylelint from "stylelint"; 2 | import printUrl from '../lib/printUrl.js'; 3 | 4 | const { 5 | createPlugin, 6 | utils: { report, ruleMessages, validateOptions } 7 | } = stylelint; 8 | 9 | const numbersRuleName = 'magic-numbers/magic-numbers'; 10 | 11 | const numbersMessages = ruleMessages(numbersRuleName, { 12 | expected: function expected(hint) { 13 | return "No-Magic-Numbers ".concat(hint); 14 | } 15 | }); 16 | 17 | const meta = { 18 | url: printUrl('magic-number') 19 | } 20 | 21 | 22 | const numbersRule = function numbersRule(actual, config) { 23 | return function (root, result) { 24 | var validOptions = validateOptions(result, numbersRuleName, { 25 | actual: actual, 26 | config: config 27 | }); 28 | 29 | if (!validOptions || !actual) { 30 | return; 31 | } 32 | 33 | var acceptedValues = config.acceptedValues || []; 34 | var acceptedNumbers = config.acceptedNumbers || []; 35 | var ignoreProperties = config.ignoreProperties || []; 36 | root.walkDecls(function (decl) { 37 | var value = decl.value; 38 | var prop = decl.prop; // ignore variables 39 | 40 | if (value.startsWith("var(") || value.startsWith("url") || value.startsWith("U+") || value.startsWith("$") || prop.startsWith("$") || prop.startsWith("--") || ignoreProperties.includes(prop)) { 41 | return; 42 | } // ignore values that are no numbers 43 | 44 | 45 | var valueRegExp = RegExp(/\d+\.?\d*(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|ms|s|fr)?|\.\d+/, 'g'); 46 | 47 | if (!valueRegExp.test(value)) { 48 | return; 49 | } 50 | 51 | var values = []; 52 | value.match(valueRegExp).forEach(function (currentValue) { 53 | if (currentValue) { 54 | values.push(currentValue); 55 | } 56 | }); 57 | var accepted = true; 58 | var failedValues = []; 59 | values.forEach(function (val) { 60 | var theNumber = val.match(/([\d.]+)/)[0]; 61 | var valOK = acceptedValues.some(e => val.match(e)) || acceptedNumbers.some(e => val.match(e)); 62 | accepted = accepted && valOK; 63 | 64 | if (!valOK) { 65 | failedValues.push(val); 66 | } 67 | }); 68 | 69 | if (accepted) { 70 | return; 71 | } // Ignore if Value is inside a String. 72 | 73 | 74 | var isStringWrapped = RegExp(/^(.*\()?['"].*['"]\)?$/g); 75 | 76 | if (isStringWrapped.test(value)) { 77 | return; 78 | } 79 | 80 | report({ 81 | index: decl.lastEach, 82 | messageArgs: [value], 83 | message: numbersMessages.expected("\"".concat(prop, ": ").concat(value, "\" -> ").concat(failedValues, " failed")), 84 | node: decl, 85 | ruleName: numbersRuleName, 86 | result: result 87 | }); 88 | }); 89 | }; 90 | }; 91 | 92 | const colorsRuleName = 'magic-numbers/magic-colors'; 93 | 94 | 95 | const colorsMessages = ruleMessages(colorsRuleName, { 96 | expected: function expected(hint) { 97 | return "No-Magic-Colors ".concat(hint); 98 | } 99 | }); 100 | 101 | 102 | const colorsRule = function colorsRule(actual) { 103 | return function (root, result) { 104 | var validOptions = validateOptions(result, numbersRuleName, { 105 | actual: actual 106 | }); 107 | 108 | if (!validOptions || !actual) { 109 | return; 110 | } 111 | 112 | root.walkDecls(function (decl) { 113 | var value = decl.value; 114 | var prop = decl.prop; // ignore variables 115 | 116 | if (value.startsWith("var(") || value.startsWith("$") || prop.startsWith("$")) { 117 | return; 118 | } // ignore values that are no colors 119 | 120 | 121 | var isColor = RegExp(/rgba?\( *\d+, *\d+, *\d+(, *0?\.?\d+)? *\)|hsla?\( *\d+, *\d+%, *\d+%(, *0?\.?\d+)? *\)|#[0-9a-f]{8}|#[0-9a-f]{6}|#[0-9a-f]{3}/, 'ig'); 122 | 123 | if (!isColor.test(value)) { 124 | return; 125 | } // Ignore if Color is inside a String. 126 | 127 | 128 | var isStringWrapped = RegExp(/^(.*\()?['"].*['"]\)?$/g); 129 | 130 | if (isStringWrapped.test(value)) { 131 | return; 132 | } 133 | 134 | report({ 135 | index: decl.lastEach, 136 | message: colorsMessages.expected("\"".concat(prop, ": ").concat(value, "\"")), 137 | node: decl, 138 | ruleName: colorsRuleName, 139 | result: result 140 | }); 141 | }); 142 | }; 143 | }; 144 | 145 | numbersRule.numbersRuleName = numbersRuleName; 146 | numbersRule.messages = numbersMessages; 147 | 148 | colorsRule.messages = colorsMessages; 149 | colorsRule.colorsRuleName = colorsRuleName; 150 | 151 | var rulesPlugins = [createPlugin(numbersRuleName, numbersRule), createPlugin(colorsRuleName, colorsRule)]; 152 | 153 | export default rulesPlugins; 154 | -------------------------------------------------------------------------------- /plugins/stylelint-z-index-value-constraint.js: -------------------------------------------------------------------------------- 1 | import stylelint from "stylelint"; 2 | 3 | const { 4 | createPlugin, 5 | utils: { report, ruleMessages, validateOptions } 6 | } = stylelint; 7 | 8 | const ruleName = "plugin/z-index-value-constraint"; 9 | 10 | const messages = ruleMessages(ruleName, { 11 | largerThanMax: expected => 12 | `Expected z-index to have maximum value of ${expected}.`, 13 | smallerThanMin: expected => 14 | `Expected z-index to have minimum value of ${expected}.` 15 | }); 16 | 17 | function isNumber(value) { 18 | return typeof value === "number"; 19 | } 20 | 21 | function isNegative(value) { 22 | return value < 0; 23 | } 24 | 25 | const _isNaN = 26 | Number.isNaN || 27 | function (value) { 28 | return value !== value; 29 | }; 30 | 31 | function possibleValueTest(value) { 32 | return isNumber(value) && !isNegative(value); 33 | } 34 | 35 | 36 | const rule = (options, secondary) => { 37 | return function (cssRoot, result) { 38 | const validOptions = validateOptions( 39 | result, 40 | ruleName, 41 | { 42 | actual: options, 43 | possible: { 44 | min: possibleValueTest, 45 | max: possibleValueTest 46 | } 47 | }, 48 | { 49 | actual: secondary, 50 | possible: { 51 | ignoreValues: [isNumber] 52 | }, 53 | optional: true 54 | } 55 | ); 56 | 57 | if (!validOptions) { 58 | return; 59 | } 60 | 61 | cssRoot.walkRules((rule) => { 62 | rule.walkDecls("z-index", function (decl) { 63 | const value = Number(decl.value); 64 | 65 | if ( 66 | _isNaN(value) || 67 | (secondary && 68 | Array.isArray(secondary.ignoreValues) && 69 | secondary.ignoreValues.indexOf(value) > -1) 70 | ) { 71 | return; 72 | } 73 | 74 | if (options.max && Math.abs(value) > options.max) { 75 | report({ 76 | messageArg: [rule.selector, options.max], 77 | ruleName, 78 | result, 79 | node: decl, 80 | message: messages.largerThanMax( 81 | isNegative(value) ? options.max * -1 : options.max 82 | ) 83 | }); 84 | } 85 | 86 | if (options.min && Math.abs(value) < options.min) { 87 | report({ 88 | ruleName, 89 | messageArg: [rule.selector, options.min], 90 | result, 91 | node: decl, 92 | message: messages.smallerThanMin( 93 | isNegative(value) ? options.min * -1 : options.min 94 | ) 95 | }); 96 | } 97 | }); 98 | }); 99 | }; 100 | } 101 | 102 | rule.ruleName = ruleName; 103 | rule.messages = messages; 104 | 105 | export default createPlugin(ruleName, rule); 106 | -------------------------------------------------------------------------------- /plugins/utils/hasPropertyValueInContext.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {object} rule 3 | * @param {string} propertyPattern 4 | * @param {RegExp|string} valuePattern 5 | * @param {string} context 6 | * @returns {boolean} 7 | */ 8 | export default function hasPropertyValueInContext(rule, propertyPattern, valuePattern, context) { 9 | 10 | const isDescendant = /&\s*(?:>|\s+\.\w+|:{1,2}(?:before|after))/.test(rule.selector); 11 | const isCombined = /&(:[\w-]+|::[\w-]+|\[.*?\]|\.[\w-]+|#\w+)/u.test(rule.selector); 12 | 13 | let currentRule = context == 'parent' && !isCombined ? rule.parent : rule; 14 | 15 | while (currentRule && currentRule.type === 'rule') { 16 | 17 | const hasPropertyValue = currentRule.some((decl) => ( 18 | propertyPattern instanceof RegExp 19 | ? propertyPattern.test(decl.prop) 20 | : decl.prop === propertyPattern 21 | ) && ( 22 | valuePattern instanceof RegExp 23 | ? valuePattern.test(decl.value) 24 | : decl.value === valuePattern 25 | )); 26 | 27 | if (hasPropertyValue) { 28 | return true; 29 | } 30 | if(context == 'self' && isCombined || context == 'parent' && isDescendant) { 31 | currentRule = currentRule.parent; 32 | } else { 33 | return false; 34 | } 35 | } 36 | return false; 37 | }; 38 | -------------------------------------------------------------------------------- /plugins/utils/isKeyframeSelector.js: -------------------------------------------------------------------------------- 1 | const keyframeSelectorKeywords = new Set(['from', 'to']); 2 | 3 | /** 4 | * Check whether a string is a keyframe selector. 5 | * 6 | * @param {string} selector 7 | * @returns {boolean} 8 | */ 9 | export default function isKeyframeSelector(selector) { 10 | if (keyframeSelectorKeywords.has(selector)) { 11 | return true; 12 | } 13 | 14 | // Percentages 15 | if (/^(?:\d+|\d*\.\d+)%$/.test(selector)) { 16 | return true; 17 | } 18 | 19 | return false; 20 | } 21 | 22 | -------------------------------------------------------------------------------- /plugins/utils/matchesStringOrRegExp.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | /** 4 | * Compares a string to a second value that, if it fits a certain convention, 5 | * is converted to a regular expression before the comparison. 6 | * If it doesn't fit the convention, then two strings are compared. 7 | * 8 | * Any strings starting and ending with `/` are interpreted 9 | * as regular expressions. 10 | */ 11 | export default function matchesStringOrRegExp( 12 | input /*: string | Array*/, 13 | comparison /*: string | Array*/ 14 | ) /*: false | { match: string, pattern: string}*/{ 15 | if (!Array.isArray(input)) { 16 | return testAgainstStringOrArray(input, comparison); 17 | } 18 | 19 | for (const inputItem of input) { 20 | const testResult = testAgainstStringOrArray(inputItem, comparison); 21 | if (testResult) { 22 | return testResult; 23 | } 24 | } 25 | 26 | return false; 27 | }; 28 | 29 | function testAgainstStringOrArray(value, comparison) { 30 | if (!Array.isArray(comparison)) { 31 | return testAgainstString(value, comparison); 32 | } 33 | 34 | for (const comparisonItem of comparison) { 35 | const testResult = testAgainstString(value, comparisonItem); 36 | if (testResult) { 37 | return testResult; 38 | } 39 | } 40 | return false; 41 | } 42 | 43 | function testAgainstString(value, comparison) { 44 | const firstComparisonChar = comparison[0]; 45 | const lastComparisonChar = comparison[comparison.length - 1]; 46 | const secondToLastComparisonChar = comparison[comparison.length - 2]; 47 | 48 | const comparisonIsRegex = 49 | firstComparisonChar === "/" && 50 | (lastComparisonChar === "/" || 51 | (secondToLastComparisonChar === "/" && lastComparisonChar === "i")); 52 | 53 | const hasCaseInsensitiveFlag = 54 | comparisonIsRegex && lastComparisonChar === "i"; 55 | 56 | if (comparisonIsRegex) { 57 | const valueMatches = hasCaseInsensitiveFlag 58 | ? new RegExp(comparison.slice(1, -2), "i").test(value) 59 | : new RegExp(comparison.slice(1, -1)).test(value); 60 | return valueMatches ? { match: value, pattern: comparison } : false; 61 | } 62 | 63 | return value === comparison ? { match: value, pattern: comparison } : false; 64 | } 65 | -------------------------------------------------------------------------------- /plugins/utils/optionsMatches.js: -------------------------------------------------------------------------------- 1 | import matchesStringOrRegExp from './matchesStringOrRegExp.js'; 2 | 3 | export default function optionsMatches(options, propertyName, input) { 4 | return Boolean( 5 | options && 6 | options[propertyName] && 7 | typeof input === 'string' && 8 | matchesStringOrRegExp(input, options[propertyName]), 9 | ); 10 | } 11 | 12 | -------------------------------------------------------------------------------- /plugins/utils/syntax-extension/index.js: -------------------------------------------------------------------------------- 1 | export { default as less } from './less/index.js'; 2 | export { default as sass } from './sass/index.js'; 3 | -------------------------------------------------------------------------------- /plugins/utils/syntax-extension/less/LessEscaping.js: -------------------------------------------------------------------------------- 1 | import { tokenTypes } from 'css-tree'; 2 | 3 | const STRING = tokenTypes.String; 4 | const TILDE = 0x007E; // U+007E TILDE (~) 5 | 6 | export const name = 'LessEscaping'; 7 | export const structure = { 8 | value: 'String' 9 | }; 10 | export function parse() { 11 | const start = this.tokenStart; 12 | 13 | this.eatDelim(TILDE); 14 | 15 | return { 16 | type: 'LessEscaping', 17 | loc: this.getLocation(start, this.tokenEnd), 18 | value: this.consume(STRING) 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /plugins/utils/syntax-extension/less/LessNamespace.js: -------------------------------------------------------------------------------- 1 | import { tokenTypes } from 'css-tree'; 2 | 3 | const HASH = tokenTypes.Hash; 4 | const FUNCTION = tokenTypes.Function; 5 | const IDENT = tokenTypes.Ident; 6 | const FULLSTOP = 0x002E; // U+002E FULL STOP (.) 7 | const GREATERTHANSIGN = 0x003E; // U+003E GREATER-THAN SIGN (>) 8 | 9 | function consumeRaw() { 10 | return this.createSingleNodeList( 11 | this.Raw(this.tokenIndex, null, false) 12 | ); 13 | } 14 | 15 | export const name = 'LessNamespace'; 16 | export const structure = { 17 | name: 'Identifier', 18 | member: ['Function', 'Identifier'] 19 | }; 20 | export function parse() { 21 | const start = this.tokenStart; 22 | const name = this.consume(HASH).substr(1); 23 | let member; 24 | 25 | this.skipSC(); // deprecated 26 | 27 | 28 | // deprecated 29 | if (this.isDelim(GREATERTHANSIGN)) { 30 | this.next(); 31 | this.skipSC(); 32 | } 33 | 34 | this.eatDelim(FULLSTOP); 35 | 36 | switch (this.tokenType) { 37 | case FUNCTION: 38 | member = this.Function(consumeRaw, this.scope.Value); 39 | break; 40 | 41 | case IDENT: 42 | member = this.Identifier(); 43 | break; 44 | 45 | default: 46 | this.error('Function or ident expected'); 47 | } 48 | 49 | return { 50 | type: 'LessNamespace', 51 | loc: this.getLocation(start, this.tokenEnd), 52 | name, 53 | member 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /plugins/utils/syntax-extension/less/LessVariable.js: -------------------------------------------------------------------------------- 1 | import { tokenTypes } from 'css-tree'; 2 | 3 | const ATKEYWORD = tokenTypes.AtKeyword; 4 | 5 | export const name = 'LessVariable'; 6 | export const structure = { 7 | name: 'Identifier' 8 | }; 9 | export function parse() { 10 | const start = this.tokenStart; 11 | 12 | this.eat(ATKEYWORD); 13 | 14 | return { 15 | type: 'LessVariable', 16 | loc: this.getLocation(start, this.tokenEnd), 17 | name: this.substrToCursor(start + 1) 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /plugins/utils/syntax-extension/less/LessVariableReference.js: -------------------------------------------------------------------------------- 1 | import { tokenTypes } from 'css-tree'; 2 | 3 | const ATRULE = tokenTypes.AtKeyword; 4 | const COMMERCIALAT = 0x0040; // U+0040 COMMERCIAL AT (@) 5 | 6 | export const name = 'LessVariableReference'; 7 | export const structure = { 8 | name: 'Identifier' 9 | }; 10 | export function parse() { 11 | const start = this.tokenStart; 12 | 13 | this.eatDelim(COMMERCIALAT); 14 | this.eat(ATRULE); 15 | 16 | return { 17 | type: 'LessVariableReference', 18 | loc: this.getLocation(start, this.tokenEnd), 19 | name: this.substrToCursor(start + 2) 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /plugins/utils/syntax-extension/less/index.js: -------------------------------------------------------------------------------- 1 | import { tokenTypes as TYPE } from 'css-tree'; 2 | import * as LessVariableReference from './LessVariableReference.js'; 3 | import * as LessVariable from './LessVariable.js'; 4 | import * as LessEscaping from './LessEscaping.js'; 5 | import * as LessNamespace from './LessNamespace.js'; 6 | 7 | const FULLSTOP = 0x002E; // U+002E FULL STOP (.) 8 | const GREATERTHANSIGN = 0x003E; // U+003E GREATER-THAN SIGN (>) 9 | const COMMERCIALAT = 0x0040; // U+0040 COMMERCIAL AT (@) 10 | const TILDE = 0x007E; // U+007E TILDE (~) 11 | 12 | // custom error 13 | class PreprocessorExtensionError { 14 | constructor() { 15 | this.type = 'PreprocessorExtensionError'; 16 | } 17 | } 18 | 19 | function throwOnSyntaxExtension() { 20 | let node = null; 21 | 22 | switch (this.tokenType) { 23 | case TYPE.AtKeyword: // less: @var 24 | node = this.LessVariable(); 25 | break; 26 | 27 | case TYPE.Hash: { 28 | let sc = 0; 29 | let tokenType = 0; 30 | 31 | // deprecated 32 | do { 33 | tokenType = this.lookupType(++sc); 34 | if (tokenType !== TYPE.WhiteSpace && tokenType !== TYPE.Comment) { 35 | break; 36 | } 37 | } while (tokenType !== TYPE.EOF); 38 | 39 | if (this.isDelim(FULLSTOP, sc) || /* preferred */ 40 | this.isDelim(GREATERTHANSIGN, sc) /* deprecated */) { 41 | node = this.LessNamespace(); 42 | } 43 | 44 | break; 45 | } 46 | 47 | case TYPE.Delim: 48 | switch (this.source.charCodeAt(this.tokenStart)) { 49 | case COMMERCIALAT: // less: @@var 50 | if (this.lookupType(1) === TYPE.AtKeyword) { 51 | node = this.LessVariableReference(); 52 | } 53 | break; 54 | 55 | case TILDE: // less: ~"asd" | ~'asd' 56 | node = this.LessEscaping(); 57 | break; 58 | 59 | 60 | } 61 | 62 | break; 63 | } 64 | 65 | // currently we can't validate values that contain less/sass extensions 66 | if (node !== null) { 67 | throw new PreprocessorExtensionError(); 68 | } 69 | } 70 | 71 | export default function less(syntaxConfig) { 72 | // new node types 73 | syntaxConfig.node.LessVariableReference = LessVariableReference; 74 | syntaxConfig.node.LessVariable = LessVariable; 75 | syntaxConfig.node.LessEscaping = LessEscaping; 76 | syntaxConfig.node.LessNamespace = LessNamespace; 77 | 78 | // custom at-rules 79 | syntaxConfig.atrules.plugin = { 80 | prelude: '' 81 | }; 82 | 83 | // extend parser's at-rule preluder parser 84 | const originalAttrulePreludeGetNode = syntaxConfig.scope.AtrulePrelude.getNode; 85 | syntaxConfig.scope.AtrulePrelude.getNode = function(context) { 86 | throwOnSyntaxExtension.call(this); 87 | 88 | return originalAttrulePreludeGetNode.call(this, context); 89 | }; 90 | 91 | // extend parser's value parser 92 | const originalValueGetNode = syntaxConfig.scope.Value.getNode; 93 | syntaxConfig.scope.Value.getNode = function(context) { 94 | throwOnSyntaxExtension.call(this); 95 | 96 | return originalValueGetNode.call(this, context); 97 | }; 98 | 99 | return syntaxConfig; 100 | } 101 | -------------------------------------------------------------------------------- /plugins/utils/syntax-extension/sass/SassInterpolation.js: -------------------------------------------------------------------------------- 1 | import { List, tokenTypes } from 'css-tree'; 2 | 3 | const LEFTCURLYBRACKET = tokenTypes.LeftCurlyBracket; 4 | const RIGHTCURLYBRACKET = tokenTypes.RightCurlyBracket; 5 | const NUMBERSIGN = 0x0023; // U+0023 NUMBER SIGN (#) 6 | 7 | export const name = 'SassInterpolation'; 8 | export const structure = { 9 | children: [[]] 10 | }; 11 | export function parse(recognizer, readSequence) { 12 | const start = this.tokenStart; 13 | let children = new List(); 14 | 15 | this.eatDelim(NUMBERSIGN); 16 | this.eat(LEFTCURLYBRACKET); 17 | children = readSequence.call(this, recognizer); 18 | this.eat(RIGHTCURLYBRACKET); 19 | 20 | return { 21 | type: 'SassInterpolation', 22 | loc: this.getLocation(start, this.tokenStart), 23 | children 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /plugins/utils/syntax-extension/sass/SassNamespace.js: -------------------------------------------------------------------------------- 1 | import { tokenTypes } from 'css-tree'; 2 | 3 | const FUNCTION = tokenTypes.Function; 4 | const IDENT = tokenTypes.Ident; 5 | const DOLLARSIGN = 0x0024; // U+0024 DOLLAR SIGN ($) 6 | const FULLSTOP = 0x002E; // U+002E FULL STOP (.) 7 | 8 | function consumeRaw() { 9 | return this.createSingleNodeList( 10 | this.Raw(this.tokenIndex, null, false) 11 | ); 12 | } 13 | 14 | export const name = 'SassNamespace'; 15 | export const structure = { 16 | name: 'Identifier', 17 | member: ['Function', 'Ident'] 18 | }; 19 | export function parse() { 20 | const start = this.tokenStart; 21 | const name = this.consume(IDENT); 22 | let member; 23 | 24 | this.eatDelim(FULLSTOP); 25 | 26 | switch (this.tokenType) { 27 | case FUNCTION: 28 | member = this.Function(consumeRaw, this.scope.Value); 29 | break; 30 | 31 | default: 32 | if (this.isDelim(DOLLARSIGN)) { 33 | member = this.SassVariable(); 34 | } else { 35 | this.error('Function or ident expected'); 36 | } 37 | } 38 | 39 | return { 40 | type: 'SassNamespace', 41 | loc: this.getLocation(start, this.tokenEnd), 42 | name, 43 | member 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /plugins/utils/syntax-extension/sass/SassVariable.js: -------------------------------------------------------------------------------- 1 | import { tokenTypes } from 'css-tree'; 2 | const IDENT = tokenTypes.Ident; 3 | const DOLLARSIGN = 0x0024; // U+0024 DOLLAR SIGN ($) 4 | 5 | export const name = 'SassVariable'; 6 | export const structure = { 7 | name: 'Identifier' 8 | }; 9 | export function parse() { 10 | const start = this.tokenStart; 11 | 12 | this.eatDelim(DOLLARSIGN); 13 | 14 | return { 15 | type: 'SassVariable', 16 | loc: this.getLocation(start, this.tokenEnd), 17 | name: this.consume(IDENT) 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /plugins/utils/syntax-extension/sass/index.js: -------------------------------------------------------------------------------- 1 | import { tokenTypes as TYPE } from 'css-tree'; 2 | import * as SassVariable from './SassVariable.js'; 3 | import * as SassInterpolation from './SassInterpolation.js'; 4 | import * as SassNamespace from './SassNamespace.js'; 5 | 6 | const NUMBERSIGN = 0x0023; // U+0023 NUMBER SIGN (#) 7 | const DOLLARSIGN = 0x0024; // U+0024 DOLLAR SIGN ($) 8 | const PERCENTAGESIGN = 0x0025; // U+0025 PERCENTAGE SIGN (%) 9 | const FULLSTOP = 0x002E; // U+002E FULL STOP (.) 10 | 11 | // custom error 12 | class PreprocessorExtensionError { 13 | constructor() { 14 | this.type = 'PreprocessorExtensionError'; 15 | } 16 | } 17 | 18 | function throwOnSyntaxExtension() { 19 | let node = null; 20 | 21 | switch (this.tokenType) { 22 | case TYPE.Ident: 23 | if (this.isDelim(FULLSTOP, 1)) { 24 | node = this.SassNamespace(); 25 | } 26 | break; 27 | 28 | case TYPE.Delim: 29 | switch (this.source.charCodeAt(this.tokenStart)) { 30 | case DOLLARSIGN: // sass: $var 31 | node = this.SassVariable(); 32 | break; 33 | 34 | case NUMBERSIGN: // sass: #{ } 35 | if (this.lookupType(1) === TYPE.LeftCurlyBracket) { 36 | node = this.SassInterpolation(this.scope.Value, this.readSequence); 37 | } 38 | break; 39 | 40 | case PERCENTAGESIGN: // sass: 5 % 4 41 | node = this.Operator(); 42 | break; 43 | } 44 | break; 45 | } 46 | 47 | // currently we can't validate values that contain less/sass extensions 48 | if (node !== null) { 49 | throw new PreprocessorExtensionError(); 50 | } 51 | } 52 | 53 | export default function sass(syntaxConfig) { 54 | // new node types 55 | syntaxConfig.node.SassVariable = SassVariable; 56 | syntaxConfig.node.SassInterpolation = SassInterpolation; 57 | syntaxConfig.node.SassNamespace = SassNamespace; 58 | 59 | // custom at-rules 60 | syntaxConfig.atrules['at-root'] = { 61 | prelude: '' 62 | }; 63 | syntaxConfig.atrules.content = { 64 | prelude: '' 65 | }; 66 | syntaxConfig.atrules.debug = { 67 | prelude: '' 68 | }; 69 | syntaxConfig.atrules.each = { 70 | prelude: '' 71 | }; 72 | syntaxConfig.atrules.else = { 73 | prelude: '?' 74 | }; 75 | syntaxConfig.atrules.error = { 76 | prelude: '' 77 | }; 78 | syntaxConfig.atrules.extend = { 79 | prelude: '' 80 | }; 81 | syntaxConfig.atrules.for = { 82 | prelude: '' 83 | }; 84 | syntaxConfig.atrules.forward = { 85 | prelude: '' 86 | }; 87 | syntaxConfig.atrules.function = { 88 | prelude: '' 89 | }; 90 | syntaxConfig.atrules.if = { 91 | prelude: '' 92 | }; 93 | syntaxConfig.atrules.import = { 94 | prelude: syntaxConfig.atrules.import.prelude + '| #' // FIXME: fix prelude extension in css-tree 95 | }; 96 | syntaxConfig.atrules.include = { 97 | prelude: '' 98 | }; 99 | syntaxConfig.atrules.mixin = { 100 | prelude: '' 101 | }; 102 | syntaxConfig.atrules.return = { 103 | prelude: '' 104 | }; 105 | syntaxConfig.atrules.use = { 106 | prelude: '' 107 | }; 108 | syntaxConfig.atrules.warn = { 109 | prelude: '' 110 | }; 111 | syntaxConfig.atrules.while = { 112 | prelude: '' 113 | }; 114 | 115 | // extend parser's at-rule preluder parser 116 | const originalAttrulePreludeGetNode = syntaxConfig.scope.AtrulePrelude.getNode; 117 | syntaxConfig.scope.AtrulePrelude.getNode = function(context) { 118 | throwOnSyntaxExtension.call(this); 119 | 120 | return originalAttrulePreludeGetNode.call(this, context); 121 | }; 122 | 123 | // extend parser's value parser 124 | const originalValueGetNode = syntaxConfig.scope.Value.getNode; 125 | syntaxConfig.scope.Value.getNode = function(context) { 126 | throwOnSyntaxExtension.call(this); 127 | 128 | return originalValueGetNode.call(this, context); 129 | }; 130 | 131 | return syntaxConfig; 132 | }; 133 | -------------------------------------------------------------------------------- /plugins/utils/validateTypes.js: -------------------------------------------------------------------------------- 1 | import isPlainObject from "is-plain-object"; 2 | 3 | 4 | /** 5 | * Checks if the value is a boolean or a Boolean object. 6 | * @param {unknown} value 7 | * @returns {value is boolean} 8 | */ 9 | function isBoolean(value) { 10 | return typeof value === 'boolean' || value instanceof Boolean; 11 | } 12 | 13 | /** 14 | * Checks if the value is a function or a Function object. 15 | * @param {unknown} value 16 | * @returns {value is Function} 17 | */ 18 | function isFunction(value) { 19 | return typeof value === 'function' || value instanceof Function; 20 | } 21 | 22 | /** 23 | * Checks if the value is *nullish*. 24 | * @see https://developer.mozilla.org/en-US/docs/Glossary/Nullish 25 | * @param {unknown} value 26 | * @returns {value is null | undefined} 27 | */ 28 | function isNullish(value) { 29 | return value == null; 30 | } 31 | 32 | /** 33 | * Checks if the value is a number or a Number object. 34 | * @param {unknown} value 35 | * @returns {value is number} 36 | */ 37 | function isNumber(value) { 38 | return typeof value === 'number' || value instanceof Number; 39 | } 40 | 41 | /** 42 | * Checks if the value is an object. 43 | * @param {unknown} value 44 | * @returns {value is object} 45 | */ 46 | function isObject(value) { 47 | return value !== null && typeof value === 'object'; 48 | } 49 | 50 | /** 51 | * Checks if the value is a regular expression. 52 | * @param {unknown} value 53 | * @returns {value is RegExp} 54 | */ 55 | function isRegExp(value) { 56 | return value instanceof RegExp; 57 | } 58 | 59 | /** 60 | * Checks if the value is a string or a String object. 61 | * @param {unknown} value 62 | * @returns {value is string} 63 | */ 64 | function isString(value) { 65 | return typeof value === 'string' || value instanceof String; 66 | } 67 | 68 | /** 69 | * Assert that the value is truthy. 70 | * @param {unknown} value 71 | * @param {string} [message] 72 | * @returns {asserts value} 73 | */ 74 | function assert(value, message = undefined) { 75 | if (message) { 76 | // eslint-disable-next-line no-console 77 | console.assert(value, message); 78 | } else { 79 | // eslint-disable-next-line no-console 80 | console.assert(value); 81 | } 82 | } 83 | 84 | /** 85 | * Assert that the value is a function or a Function object. 86 | * @param {unknown} value 87 | * @returns {asserts value is Function} 88 | */ 89 | function assertFunction(value) { 90 | // eslint-disable-next-line no-console 91 | console.assert(isFunction(value), `"${value}" must be a function`); 92 | } 93 | 94 | /** 95 | * Assert that the value is a number or a Number object. 96 | * @param {unknown} value 97 | * @returns {asserts value is number} 98 | */ 99 | function assertNumber(value) { 100 | // eslint-disable-next-line no-console 101 | console.assert(isNumber(value), `"${value}" must be a number`); 102 | } 103 | 104 | /** 105 | * Assert that the value is a string or a String object. 106 | * @param {unknown} value 107 | * @returns {asserts value is string} 108 | */ 109 | function assertString(value) { 110 | // eslint-disable-next-line no-console 111 | console.assert(isString(value), `"${value}" must be a string`); 112 | } 113 | 114 | export default { 115 | isBoolean, 116 | isFunction, 117 | isNullish, 118 | isNumber, 119 | isObject, 120 | isRegExp, 121 | isString, 122 | isPlainObject, 123 | 124 | assert, 125 | assertFunction, 126 | assertNumber, 127 | assertString, 128 | }; 129 | -------------------------------------------------------------------------------- /plugins/utils/vendorPrefixes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains helpers for working with vendor prefixes. 3 | * 4 | * @namespace vendor 5 | */ 6 | const vendor = { 7 | /** 8 | * Returns the vendor prefix extracted from an input string. 9 | * 10 | * @param {string} prop String with or without vendor prefix. 11 | * 12 | * @return {string} vendor prefix or empty string 13 | * 14 | * @example 15 | * vendorPrefixes.prefix('-moz-tab-size') //=> '-moz-' 16 | * vendorPrefixes.prefix('tab-size') //=> '' 17 | */ 18 | prefix(prop) { 19 | const match = prop.match(/^(-\w+-)/); 20 | if (match) { 21 | return match[0]; 22 | } 23 | 24 | return ""; 25 | }, 26 | 27 | /** 28 | * Returns the input string stripped of its vendor prefix. 29 | * 30 | * @param {string} prop String with or without vendor prefix. 31 | * 32 | * @return {string} String name without vendor prefixes. 33 | * 34 | * @example 35 | * vendorPrefixes.unprefixed('-moz-tab-size') //=> 'tab-size' 36 | */ 37 | unprefixed(prop) { 38 | return prop.replace(/^-\w+-/, ""); 39 | } 40 | }; 41 | 42 | export default vendor; 43 | --------------------------------------------------------------------------------